Gonna try something dangerous

This commit is contained in:
Fergal Moran
2023-08-20 01:24:45 +01:00
parent 60abea64c5
commit fefe7adc5b
36 changed files with 1467 additions and 2885 deletions

View File

@@ -5,21 +5,18 @@
"scripts": { "scripts": {
"build": "next build", "build": "next build",
"dev": "next dev", "dev": "next dev",
"devq": "concurrently 'next dev' 'quirrel'",
"ssl": "NODE_ENV=development node ./server.js", "ssl": "NODE_ENV=development node ./server.js",
"sslq": "concurrently 'NODE_ENV=development node ./server.js' 'quirrel'",
"quirrel": "quirrel",
"lint": "next lint", "lint": "next lint",
"start": "next start", "start": "next start",
"test": "export $(xargs < .env) && jest" "test": "export $(xargs < .env) && jest"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^1.7.16", "@headlessui/react": "^1.7.17",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-toast": "^1.1.4", "@radix-ui/react-toast": "^1.1.4",
"@radix-ui/react-tooltip": "^1.0.6", "@radix-ui/react-tooltip": "^1.0.6",
"@sentry/nextjs": "^7.62.0", "@sentry/nextjs": "^7.64.0",
"@sindresorhus/slugify": "^2.2.1", "@sindresorhus/slugify": "^2.2.1",
"@t3-oss/env-nextjs": "^0.6.0", "@t3-oss/env-nextjs": "^0.6.0",
"@tanstack/react-query": "^4.32.6", "@tanstack/react-query": "^4.32.6",
@@ -33,29 +30,28 @@
"import-local": "^3.1.0", "import-local": "^3.1.0",
"install": "^0.13.0", "install": "^0.13.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"next": "13.4.13", "next": "13.4.17",
"next-auth": "^4.22.5", "next-auth": "^4.23.1",
"pg": "^8.11.2", "pg": "^8.11.3",
"pino": "^8.15.0", "pino": "^8.15.0",
"pino-logflare": "^0.4.0", "pino-logflare": "^0.4.0",
"postgres": "^3.3.5", "postgres": "^3.3.5",
"quirrel": "^1.14.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-icons": "^4.10.1", "react-icons": "^4.10.1",
"superjson": "1.13.1", "superjson": "1.13.1",
"yup": "^1.2.0", "yup": "^1.2.0",
"zod": "^3.21.4", "zod": "^3.22.1",
"zustand": "^4.4.1" "zustand": "^4.4.1"
}, },
"devDependencies": { "devDependencies": {
"@azure/identity": "^3.2.4", "@azure/identity": "^3.3.0",
"@azure/storage-blob": "^12.15.0", "@azure/storage-blob": "^12.15.0",
"@faker-js/faker": "^8.0.2", "@faker-js/faker": "^8.0.2",
"@hookform/resolvers": "^3.2.0", "@hookform/resolvers": "^3.2.0",
"@ianvs/prettier-plugin-sort-imports": "^4.1.0", "@ianvs/prettier-plugin-sort-imports": "^4.1.0",
"@next/env": "^13.4.13", "@next/env": "^13.4.17",
"@radix-ui/react-avatar": "^1.0.3", "@radix-ui/react-avatar": "^1.0.3",
"@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
@@ -65,37 +61,37 @@
"@types/eslint": "^8.44.2", "@types/eslint": "^8.44.2",
"@types/jest": "^29.5.3", "@types/jest": "^29.5.3",
"@types/mime-types": "^2.1.1", "@types/mime-types": "^2.1.1",
"@types/node": "^20.4.9", "@types/node": "^20.5.0",
"@types/pg": "^8.10.2", "@types/pg": "^8.10.2",
"@types/react": "^18.2.19", "@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@types/superagent": "^4.1.18", "@types/superagent": "^4.1.18",
"@types/uuid": "^9.0.2", "@types/uuid": "^9.0.2",
"@typescript-eslint/eslint-plugin": "^6.3.0", "@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.3.0", "@typescript-eslint/parser": "^6.4.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.15",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"concurrently": "^8.2.0", "concurrently": "^8.2.0",
"dayjs": "^1.11.9", "dayjs": "^1.11.9",
"eslint": "^8.46.0", "eslint": "^8.47.0",
"eslint-config-next": "^13.4.13", "eslint-config-next": "^13.4.17",
"hls.js": "^1.4.10", "hls.js": "^1.4.10",
"jest": "^29.6.2", "jest": "^29.6.2",
"lucide-react": "^0.265.0", "lucide-react": "^0.268.0",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"node-mocks-http": "^1.12.2", "node-mocks-http": "^1.13.0",
"postcss": "^8.4.27", "pino-pretty": "^10.2.0",
"prettier": "3.0.1", "postcss": "^8.4.28",
"prettier-plugin-tailwindcss": "^0.4.1", "prettier": "3.0.2",
"prettier-plugin-tailwindcss": "^0.5.3",
"pusher": "^5.1.3", "pusher": "^5.1.3",
"pusher-js": "^8.3.0", "pusher-js": "^8.3.0",
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-hook-form": "^7.45.4", "react-hook-form": "^7.45.4",
"recharts": "^2.7.3",
"retry": "^0.13.1", "retry": "^0.13.1",
"superagent": "^8.0.9", "superagent": "^8.1.2",
"tailwind-merge": "^1.14.0", "tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"tailwindcss-animate": "^1.0.6", "tailwindcss-animate": "^1.0.6",
@@ -103,7 +99,7 @@
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"vitest": "^0.34.1" "vitest": "^0.34.2"
}, },
"ct3aMetadata": { "ct3aMetadata": {
"initVersion": "7.12.2" "initVersion": "7.12.2"

2892
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,20 @@
"use client"; "use client";
import Loading from "@/lib/components/widgets/Loading"; import Loading from "@/lib/components/widgets/Loading";
import { api } from "@/lib/utils/api";
import { type NextPage } from "next"; import { type NextPage } from "next";
import React from "react"; import React from "react";
import ProfileEditForm from "../../components/profile-edit-form"; import ProfileEditForm from "../../components/profile-edit-form";
const ProfileSettingsPage: NextPage = () => { const ProfileSettingsPage: NextPage = () => {
const { data: profile, status } = api.user.getProfileForSettings.useQuery(); return <Loading />;
// if (status === "loading") {
if (status === "loading") { // return <Loading />;
return <Loading />; // }
} // return profile ? (
return profile ? ( // <ProfileEditForm profile={profile} />
<ProfileEditForm profile={profile} /> // ) : (
) : ( // <div>Nothing here bai!</div>
<div>Nothing here bai!</div> // );
);
}; };
export default ProfileSettingsPage; export default ProfileSettingsPage;

View File

@@ -1,19 +1,17 @@
"use client";
import LargeAudioPlayer from "@/components/widgets/audio/large-audio-player"; import LargeAudioPlayer from "@/components/widgets/audio/large-audio-player";
import React from "react"; import React from "react";
import { api } from "@/lib/utils/api";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import MixService from "@/lib/services/api/mix-service";
export default function Page({ export default async function Page({
params, params,
}: { }: {
params: { username: string; mixSlug: string }; params: { username: string; mixSlug: string };
}) { }) {
const mixQuery = api.mix.getByUserAndSlug.useQuery({ const mix = await new MixService().getByUserAndSlug(
username: params.username, params.username,
mixSlug: params.mixSlug, params.mixSlug,
}); );
const mix = mixQuery?.data;
return ( return (
<div className="container p-5"> <div className="container p-5">
{mix ? ( {mix ? (

View File

@@ -1,4 +1,4 @@
import { authOptions } from "@/lib/services/api/config"; import { authOptions } from "@/lib/services/auth/config";
import NextAuth from "next-auth"; import NextAuth from "next-auth";
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment

View File

@@ -1,5 +0,0 @@
import { NextResponse } from "next/server";
export function GET() {
return NextResponse.json({ debug: "deez nuts" });
}

View File

@@ -1,28 +0,0 @@
import { processMixQueue } from "@/app/api/queues/upload/mix/route";
import { StatusCodes } from "http-status-codes";
import { type NextRequest } from "next/server";
import path from "path";
import os from "os";
import fs from "fs";
const extensions = ["mp3", "wav", "aiff", "mp4", "ogg"];
export async function POST(req: NextRequest) {
const { mixId } = (await req.json()) as { mixId: string };
let fileName = "";
let exists = false;
for (const extension of extensions) {
fileName = path.join(os.tmpdir(), `${mixId}.${extension}`);
if (fs.existsSync(fileName)) {
exists = true;
break;
}
}
if (exists) {
await processMixQueue.enqueue({ filePath: fileName, mixId }, { delay: 1 });
return StatusCodes.OK;
}
return StatusCodes.NO_CONTENT;
}

View File

@@ -1,60 +0,0 @@
import { waitForShowQueue } from "@/app/api/queues/shows/wait/route";
import { LiveShowStatus, liveShows, users } from "@/db/schema";
import { db } from "@/server/db";
import { and, eq, or } from "drizzle-orm";
import { StatusCodes } from "http-status-codes";
import { NextResponse, type NextRequest } from "next/server";
export async function POST(req: NextRequest) {
if (!req.body) {
return NextResponse.json(
{ message: "No stream key found in request!" },
{ status: StatusCodes.BAD_REQUEST }
);
}
const data = await req.formData();
const streamKey = data.get("name")?.toString();
if (!streamKey) {
return NextResponse.json(
{ message: "No stream key found in request!" },
{ status: StatusCodes.BAD_REQUEST }
);
}
const results = await db
.selectDistinct()
.from(users)
.where(eq(users.streamKey, streamKey));
const user = results[0];
if (!user) {
return NextResponse.json(
{ message: "Unauthorised!" },
{ status: StatusCodes.UNAUTHORIZED }
);
}
const showResults = await db
.selectDistinct()
.from(liveShows)
.where(
and(
eq(liveShows.userId, user.id),
or(eq(liveShows.status, "AWAITING"), eq(liveShows.status, "STREAMING"))
)
);
const show = showResults[0];
if (!show) {
return NextResponse.json(
{ message: "No in progress show found for user!" },
{ status: StatusCodes.BAD_REQUEST }
);
}
await waitForShowQueue.enqueue(show.id, { delay: 1 });
return new NextResponse("Redirecting.....", {
status: StatusCodes.MOVED_TEMPORARILY,
headers: {
"Content-Type": "text/plain",
Location: show.id,
},
});
}

View File

@@ -1,62 +0,0 @@
import { liveShows } from "@/db/schema";
import { ShowStatus } from "@/lib/models";
import { createPusherServer } from "@/lib/services/realtime";
import { showRouter } from "@/server/api/routers/show";
import { userRouter } from "@/server/api/routers/user";
import { createTRPCContext } from "@/server/api/trpc";
import { db } from "@/server/db";
import { eq } from "drizzle-orm";
import { StatusCodes } from "http-status-codes";
import { type NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
// Process a POST request
// const ctx = await createTRPCContext({ req, res });
// const userApi = userRouter.createCaller(ctx);
// const showApi = showRouter.createCaller(ctx);
// const { name: streamKey } = req.body as { name: string };
// if (!streamKey) {
// return new NextResponse(
// JSON.stringify({ message: "No stream key found in request!" }),
// { status: StatusCodes.BAD_REQUEST },
// );
// }
// const user = await userApi.getByStreamKey({ streamKey });
// if (!user) {
// return new NextResponse(JSON.stringify({ message: "Unauthorised!" }), {
// status: StatusCodes.UNAUTHORIZED,
// });
// }
// const show = await showApi.checkForInProgress({ userId: user.id });
// if (!show) {
// return new NextResponse(
// JSON.stringify({ message: "No in progress show found for user!!" }),
// {
// status: StatusCodes.BAD_REQUEST,
// },
// );
// }
// //show go bye bye
// /*
// await scheduler.TriggerJob(new JobKey("SaveLiveShowJob", "DEFAULT"), new JobDataMap(
// new Dictionary<string, string> {
// {"ShowId", show.Id.ToString()}
// }
// ));
// */
// show.status = ShowStatus.ended;
// await db
// .update(liveShows)
// .set({ ...show, status: "FINISHED" })
// .where(eq(liveShows.id, show.id));
// await createPusherServer().trigger(`ls_${show.id}`, "show-finished", show);
// return new NextResponse(JSON.stringify(show), {
return new NextResponse(JSON.stringify({}), {
status: StatusCodes.OK,
});
}

View File

@@ -1,5 +0,0 @@
import { NextResponse } from "next/server";
export function GET() {
return NextResponse.json({ ping: "pong" });
}

View File

@@ -1,89 +0,0 @@
import { liveShows, users } from "@/db/schema";
import { createPusherServer } from "@/lib/services/realtime";
import { mapShowToShowModel } from "@/lib/utils/mappers/showMappers";
import { mapDbAuthUserToUserModel } from "@/lib/utils/mappers/userMapper";
import { db } from "@/server/db";
import { eq } from "drizzle-orm";
import { StatusCodes } from "http-status-codes";
import { Queue } from "quirrel/next-app";
import superagent from "superagent";
export const waitForShowQueue = Queue(
"api/queues/shows/wait", // 👈 the route it's reachable on
async (showId: string) => {
const rt = createPusherServer();
const url = `https://live-mixyboos.dev.fergl.ie:9091/hls/${showId}/index.m3u8`;
console.log("wait", "Checking URL", url);
const results = await db
.selectDistinct()
.from(liveShows)
.where(eq(liveShows.id, showId));
const show = results[0];
if (!show) {
throw new Error(`Unable to find show ${showId}`);
}
const result = await db
.selectDistinct()
.from(users)
.where(eq(users.id, show.userId));
if (!result || !result[0]) {
throw new Error(`Unable to find user ${show.userId}`);
}
const user = mapDbAuthUserToUserModel(result[0]);
if (!user) {
throw new Error(`Unable to find user ${show.userId}`);
}
const res = await superagent
.get(url)
.retry(10, (err, res) => {
console.log("wait", "result", res.notFound, res.statusCode, res.status);
const shouldRetry = res.notFound;
if (shouldRetry) {
console.log("wait", "Waiting to retry");
Atomics.wait(
new Int32Array(new SharedArrayBuffer(4)),
0,
0,
2 * 1000,
);
return true;
}
return res.notFound;
})
.catch((err) => {
console.log("WaitForShow", "Error fetching retrying", err);
});
console.log("wait", "Finished waiting for show", res?.statusCode);
if (res?.statusCode === StatusCodes.OK) {
//showtime
//update mix entry in db
await db
.update(liveShows)
.set({
...show,
status: "STREAMING",
})
.where(eq(liveShows.id, show.id));
//push status to client
await rt.trigger(`ls_${showId}`, "show-started", {
show: mapShowToShowModel(show, user),
});
console.log("waitForShow", "show-started", showId);
return;
}
//notime
await rt.trigger(`ls_${showId}`, "show-failure", {
id: showId,
});
console.error("waitForShow", "FAILED: show-failure", showId);
},
);
export const POST = waitForShowQueue;

View File

@@ -1,65 +0,0 @@
import { spawn } from "child_process";
import fs from "fs";
import os from "os";
import path from "path";
import { uploadFolder } from "@/lib/services/azure/serverUploader";
import { Queue } from "quirrel/next-app";
import { db } from "@/server/db";
import { mixes } from "@/db/schema";
import { eq } from "drizzle-orm";
import * as Sentry from "@sentry/nextjs";
export const processMixQueue = Queue(
"api/queues/upload/mix", // 👈 the route it's reachable on
async (job: { filePath: string; mixId: string }) => {
const { filePath, mixId } = job;
const outputDir = `${os.tmpdir()}/podnoms/${mixId}`;
fs.mkdirSync(outputDir, {
recursive: true,
});
// prettier-ignore
const process = spawn("ffmpeg", [
"-i",
filePath,
"-codec:", "copy",
"-start_number", "0",
"-hls_time", "10",
"-hls_list_size", "0",
"-f", "hls",
`${outputDir}/${mixId}.m3u8`,
]);
process.stdout.on("data", (data) => {
console.log("processMixQueue", "stdout", data);
});
process.stderr.on("data", (data) => {
console.log("processMixQueue", "stderr", data);
});
process.on("close", (code) => {
console.log("processMixQueue", "close", code);
if (code !== 0) {
return;
}
uploadFolder(outputDir, "audio", path.join("mixes", mixId))
.then(async (r) => {
if (r) {
//if client has saved mix then we are processed
//if not client will check for file and set processed
await db
.update(mixes)
.set({ isProcessed: true })
.where(eq(mixes.id, mixId));
}
})
.catch((err) => {
console.log("route", "error uploading output folder", err);
Sentry.captureException(err);
});
});
}
);
export const POST = processMixQueue;

View File

@@ -1,37 +0,0 @@
import { writeFile } from "fs/promises";
import { getFileExtension } from "@/lib/utils/fileUtils";
import { NextResponse, type NextRequest } from "next/server";
import { processMixQueue } from "../queues/upload/mix/route";
import * as Sentry from "@sentry/nextjs";
import path from "path";
import os from "os";
export async function POST(req: NextRequest) {
const data = await req.formData();
const file: File | null = data.get("file") as unknown as File;
const id: string | null = data.get("mixId") as unknown as string;
if (!file) {
return NextResponse.json({ success: false });
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
const outputFile = path.join(
os.tmpdir(),
`${id}.${getFileExtension(file.name)}`
);
await writeFile(outputFile, buffer);
console.log(`open ${outputFile} to see the uploaded file`);
try {
const result = await processMixQueue.enqueue(
{ filePath: outputFile, mixId: id },
{ delay: 1 }
);
console.log('route', 'processMixSendQueueResult', result);
} catch (err) {
Sentry.captureException(err);
}
return NextResponse.json({ success: true });
}

View File

@@ -29,6 +29,7 @@ import Link from "next/link";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import logger from "@/lib/logger";
const LoginPage = () => { const LoginPage = () => {
const [loginError, setLoginError] = React.useState(false); const [loginError, setLoginError] = React.useState(false);
@@ -45,10 +46,10 @@ const LoginPage = () => {
function onSubmit(values: z.infer<typeof formSchema>) { function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values. // Do something with the form values.
// ✅ This will be type-safe and validated. // ✅ This will be type-safe and validated.
console.log(values); logger.debug(values);
setLoginError(false); setLoginError(false);
signIn("credentials", { signIn("credentials", {
email: values.usernameOrEmail, username: values.usernameOrEmail,
password: values.password, password: values.password,
callbackUrl: callbackUrl:
searchParams?.get("callbackUrl") || searchParams?.get("callbackUrl") ||
@@ -57,14 +58,16 @@ const LoginPage = () => {
redirect: false, redirect: false,
}) })
.then((result) => { .then((result) => {
if (result?.ok) { //TODO: have to check result?.error rather than result.ok
//TODO: https://github.com/nextauthjs/next-auth/issues/7725#issuecomment-1649310412
if (!result?.error) {
router.push(searchParams?.get("returnUrl") || "/"); router.push(searchParams?.get("returnUrl") || "/");
} else { } else {
setLoginError(true); setLoginError(true);
} }
}) })
.catch((err) => { .catch((err) => {
console.error("login", "handleLogin", err); logger.error("login", "handleLogin", err);
setLoginError(true); setLoginError(true);
}); });
} }
@@ -109,6 +112,13 @@ const LoginPage = () => {
</button> </button>
</div> </div>
<div className="border-sm mt-4 text-center shadow-lg">or</div> <div className="border-sm mt-4 text-center shadow-lg">or</div>
{loginError && (
<Alert variant="destructive">
<Icons.error className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Unable to sign you in</AlertDescription>
</Alert>
)}
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField <FormField
@@ -155,15 +165,7 @@ const LoginPage = () => {
</div> </div>
</Button> </Button>
</div> </div>
{loginError && (
<div>
<Alert variant="destructive">
<Icons.error className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Unable to sign you in</AlertDescription>
</Alert>
</div>
)}
<div className="text-sm font-medium text-gray-500"> <div className="text-sm font-medium text-gray-500">
Not registered? Not registered?
<Link <Link

View File

@@ -19,9 +19,12 @@ import { Icons } from "@/components/icons";
import { notice } from "@/lib/components/notifications/toast"; import { notice } from "@/lib/components/notifications/toast";
import Link from "next/link"; import Link from "next/link";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { api } from "@/lib/utils/api"; import AuthService from "@/lib/services/api/auth-service";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import logger from "@/lib/logger";
const LoginPage = () => { const LoginPage = () => {
const [hasErrors, setHasErrors] = React.useState(false);
const schema = z const schema = z
.object({ .object({
username: z username: z
@@ -42,33 +45,35 @@ const LoginPage = () => {
const form = useForm<z.infer<typeof schema>>({ const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
username: "", username: "superawesomedjperson",
email: "", email: "fergal.moran+mixyboos@gmail.com",
password: "", password: "secret",
}, confirmPassword: "secret",
});
const register = api.auth.signUp.useMutation({
onSuccess: (result) => {
console.log("page", "register_success", result);
}, },
}); });
async function onSubmit(values: z.infer<typeof schema>) { async function onSubmit(values: z.infer<typeof schema>) {
// Do something with the form values. logger.debug(values);
// ✅ This will be type-safe and validated. setHasErrors(false);
console.log(values);
try { try {
const result = await register.mutateAsync({ const result = await new AuthService().registerUser(
email: values.email, values.email,
username: values.username, values.password,
password: values.password, values.confirmPassword,
}); values.username,
console.log("page", "handleRegister", result); );
if (result?.status === 201) { if (result) {
await signIn(); const response = await signIn("credentials", {
username: values.username,
password: values.password,
callbackUrl: "/",
redirect: true,
});
logger.debug("RegisterPage", "signin_credentials", response);
} }
} catch (err) { } catch (err) {
console.error("RegisterPage", "handleLogin", err); logger.error("RegisterPage", "handleLogin", err);
setHasErrors(true);
} }
} }
return ( return (
@@ -114,6 +119,22 @@ const LoginPage = () => {
<div className="border-sm mt-4 text-center shadow-lg"> <div className="border-sm mt-4 text-center shadow-lg">
or create a new account or create a new account
</div> </div>
{hasErrors && (
<Alert variant="destructive">
<Icons.error className="h-4 w-4" />
<AlertTitle>Yikes! Something went wrong...</AlertTitle>
<AlertDescription>
Please try again, or
<Link
href="/auth/login"
className="ml-2 text-fuchsia-600 hover:underline"
>
login{" "}
</Link>
if you already have an account?
</AlertDescription>
</Alert>
)}
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField <FormField

View File

@@ -1,35 +1,10 @@
"use client"; import Session from "@/lib/components/debug/Session";
import { getServerSession } from "next-auth";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { Switch } from "@headlessui/react";
import { BellRing, Check } from "lucide-react";
import { useSession } from "next-auth/react";
import React from "react"; import React from "react";
const IndexPage = () => { const IndexPage = () => {
const session = useSession(); const session = getServerSession();
return ( return <Session serverSession={session} />;
<div className="container mt-10 flex w-3/4">
<Card className="w-full">
<CardHeader>
<CardTitle>Session</CardTitle>
<CardDescription>Client side session.</CardDescription>
</CardHeader>
<CardContent className="grid gap-4 text-muted">
{JSON.stringify(session, null, " ")}
</CardContent>
</Card>
</div>
);
}; };
export default IndexPage; export default IndexPage;

View File

@@ -2,7 +2,7 @@ import HeroPage from "@/lib/components/pages/HeroPage";
import React from "react"; import React from "react";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { authOptions } from "@/lib/services/api/config"; import { authOptions } from "@/lib/services/auth/config";
const Home = async () => { const Home = async () => {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);

View File

@@ -1,80 +1,88 @@
"use client"; "use client";
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts"; import React from "react";
const data = [ const PlaysGraph: React.FC = () => {
{ return <div>PlaysGraph</div>;
name: "Jan",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Feb",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Mar",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Apr",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "May",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Jun",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Jul",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Aug",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Sep",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Oct",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Nov",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Dec",
total: Math.floor(Math.random() * 5000) + 1000,
},
];
const PlaysGraph = () => {
return (
<ResponsiveContainer width="100%" height={350}>
<BarChart data={data}>
<XAxis
dataKey="name"
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `$${value as string}`}
/>
<Bar dataKey="total" fill="#adfa1d" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
);z
}; };
export default PlaysGraph; export default PlaysGraph;
// import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts";
// const data = [
// {
// name: "Jan",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Feb",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Mar",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Apr",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "May",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Jun",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Jul",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Aug",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Sep",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Oct",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Nov",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Dec",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// ];
// const PlaysGraph = () => {
// return (
// <ResponsiveContainer width="100%" height={350}>
// <BarChart data={data}>
// <XAxis
// dataKey="name"
// stroke="#888888"
// fontSize={12}
// tickLine={false}
// axisLine={false}
// />
// <YAxis
// stroke="#888888"
// fontSize={12}
// tickLine={false}
// axisLine={false}
// tickFormatter={(value) => `$${value as string}`}
// />
// <Bar dataKey="total" fill="#adfa1d" radius={[4, 4, 0, 0]} />
// </BarChart>
// </ResponsiveContainer>
// );
// };
// export default PlaysGraph;

View File

@@ -0,0 +1,23 @@
"use client";
import React from "react";
import SessionPrinter from "./SessionPrinter";
import { useSession } from "next-auth/react";
type SessionProps = {
serverSession: any;
};
const Session: React.FC<SessionProps> = ({ serverSession }) => {
const session = useSession();
return (
<div>
<SessionPrinter
session={serverSession}
sessionType={"Server side session."}
/>
<SessionPrinter session={session} sessionType={"Client side session."} />
</div>
);
};
export default Session;

View File

@@ -0,0 +1,34 @@
import React from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
type SessionPrinterProps = {
sessionType: string;
session: any;
};
const SessionPrinter: React.FC<SessionPrinterProps> = ({
sessionType,
session,
}: SessionPrinterProps) => {
return (
<div className="container mt-10 flex w-3/4">
<Card className="w-full">
<CardHeader>
<CardTitle>Session</CardTitle>
<CardDescription>{sessionType}</CardDescription>
</CardHeader>
<CardContent className="text-muted grid gap-4">
{JSON.stringify(session, null, " ")}
</CardContent>
</Card>
</div>
);
};
export default SessionPrinter;

View File

@@ -1,31 +1,14 @@
import pino from "pino"; import pino from "pino";
import { logflarePinoVercel } from "pino-logflare";
// create pino-logflare console stream for serverless functions and send function for browser logs const mixin = {
// Browser logs are going to: https://logflare.app/sources/13989 appName: "MixyBoos",
// Vercel log drain was setup to send logs here: https://logflare.app/sources/13830 target: 'pino-pretty'
};
const { stream, send } = logflarePinoVercel({ // create pino logger
apiKey: "MK3qgU_-pwHQ", const logger = pino({
sourceToken: "2c94c62f-012d-4891-8ef5-38103089e4af", mixin() {
return mixin;
},
}); });
// create pino logger
const logger = pino(
{
browser: {
transmit: {
level: "info",
send,
},
},
level: "info",
base: {
env: process.env.NODE_ENV,
revision: process.env.VERCEL_GITHUB_COMMIT_SHA,
},
},
stream,
);
export default logger; export default logger;

View File

@@ -5,5 +5,12 @@ type AuthTokenModel = {
expires_in: number; expires_in: number;
token_type: string; token_type: string;
expiration_date: string; expiration_date: string;
id: string;
email: string;
name: string;
displayName: string;
slug: string;
profileImage: string;
}; };
export default AuthTokenModel; export default AuthTokenModel;

View File

@@ -35,9 +35,7 @@ class ApiService {
const session = await getSession(); const session = await getSession();
if (session || this._token) { if (session || this._token) {
config.headers = { config.headers = {
Authorization: `Bearer ${ Authorization: `Bearer ${session ? session.accessToken : this._token}`,
session ? session.user.accessToken : this._token
}`,
Accept: "application/json", Accept: "application/json",
}; };
} }

View File

@@ -1,6 +1,6 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import ApiService from "./api-service"; import ApiService from "./api-service";
import { type AuthTokenModel, UserModel } from "@/lib/models"; import type { AuthTokenModel, UserModel } from "@/lib/models";
import logger from "@/lib/logger"; import logger from "@/lib/logger";
class AuthService extends ApiService { class AuthService extends ApiService {
@@ -78,15 +78,15 @@ class AuthService extends ApiService {
return Promise.reject("Unable to log in"); return Promise.reject("Unable to log in");
}; };
registerUser = async ( registerUser = async (
userName: string, email: string,
password: string, password: string,
confirmPassword: string, confirmPassword: string,
displayName: string, username: string = "",
): Promise<boolean> => { ): Promise<boolean> => {
const url = "/account/register"; const url = "/account/register";
const result = await this._client.post( const result = await this._client.post(
url, url,
{ userName, password, confirmPassword, displayName }, { username: email, password, confirmPassword, displayName: username },
this.jsonConfig, this.jsonConfig,
); );
@@ -97,6 +97,7 @@ class AuthService extends ApiService {
} }
return false; return false;
}; };
loginUser = async ( loginUser = async (
username: string, username: string,
password: string, password: string,
@@ -106,7 +107,6 @@ class AuthService extends ApiService {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append("username", username); params.append("username", username);
params.append("password", password); params.append("password", password);
UserModel;
params.append("grant_type", process.env.AUTH_GRANT_TYPE as string); params.append("grant_type", process.env.AUTH_GRANT_TYPE as string);
params.append("scope", process.env.AUTH_SCOPE as string); params.append("scope", process.env.AUTH_SCOPE as string);
params.append("client_id", process.env.AUTH_CLIENT_ID as string); params.append("client_id", process.env.AUTH_CLIENT_ID as string);

View File

@@ -1,67 +0,0 @@
import logger from "@/lib/logger";
import { type AuthOptions } from "next-auth";
import jwt_decode, { type JwtPayload } from "jwt-decode";
import CredentialsProvider from "next-auth/providers/credentials";
import AuthService from "./auth-service";
import { type AuthTokenModel } from "@/lib/models";
import AspNetIdentityAdapter from "../auth/asp-net-identity-adapter";
export const authOptions: AuthOptions = {
adapter: AspNetIdentityAdapter(process.env.NEXT_PUBLIC_API_URL as string),
providers: [
CredentialsProvider({
name: "Email and Password",
credentials: {
userName: {
label: "Username",
type: "text",
placeholder: "Username or email address",
},
password: {
label: "Password",
type: "password",
placeholder: "Password",
},
},
authorize: async (credentials, _req): Promise<any> => {
logger.info({ authorize: "Authorizing" });
try {
if (!credentials) {
return false;
}
const authService = new AuthService();
const token = await authService.getAuthToken(
credentials.userName,
credentials.password,
);
if (!token) {
return null;
}
const decodedToken = jwt_decode<JwtPayload & AuthTokenModel>(
token.access_token,
);
if (decodedToken) {
const profile = {
id: decodedToken.sub,
// name: decodedToken.name,
// displayName: decodedToken.displayName,
// email: decodedToken.email,
// profileImage: decodedToken.profileImage,
// slug: decodedToken.slug,
accessToken: token.access_token,
accessTokenExpires: token.expires_in,
};
return profile;
} else {
return false;
}
} catch (err) {
logger.error(`Error authorizing: ${err}`);
}
return null;
},
}),
],
};

View File

@@ -0,0 +1,143 @@
import { AxiosError } from "axios";
import ApiService from "./api-service";
import { type MixModel } from "@/lib/models";
class MixService extends ApiService {
getMixes = async (): Promise<Array<MixModel>> => {
try {
const result = await this._client.get("/mix");
if (result?.status === 200) {
return result.data;
}
} catch (err) {
console.log("authService", "getMixes_error", err);
if (err instanceof AxiosError) {
if (![401, 400].includes(err.status as number))
throw new Error(err as any);
}
}
throw new Error("Unable to load mixes");
};
getMixesFeed = async (): Promise<MixModel[]> => {
try {
const result = await this._client.get("/mix/feed");
if (result?.status === 200) {
return result.data;
}
} catch (err) {
console.log("authService", "getMixes_error", err);
if (err instanceof AxiosError) {
if (![401, 400].includes(err.status as number))
throw new Error(err as any);
}
}
throw new Error("Unable to load mixes");
};
getUserMixes = async (user: string): Promise<MixModel[]> => {
try {
const result = await this._client.get(`/mix/user?user=${user}`);
if (result?.status === 200) {
return result.data;
}
if (result?.status === 204) {
return [];
}
} catch (err) {
console.log("authService", "getMixes_error", err);
if (err instanceof AxiosError) {
if (![401, 400].includes(err.status as number))
throw new Error(err as any);
}
}
throw new Error("Unable to load mixes");
};
getByUserAndSlug = async (
userSlug: string,
mixSlug: string,
): Promise<MixModel> => {
try {
const result = await this._client.get(
`/mix/single?user=${userSlug}&mix=${mixSlug}`,
);
if (result?.status === 200) {
return result.data;
}
} catch (err) {
console.log("authService", "getMixes_error", err);
if (err instanceof AxiosError) {
if (![401, 400].includes(err.status as number))
throw new Error(err as any);
}
}
throw new Error("Unable to load mixes");
};
createMix = async (mix: MixModel): Promise<MixModel> => {
try {
const result = await this._client.post("/mix", mix);
if (result?.status === 201) {
return result.data;
}
} catch (err) {
console.log("authService", "getUser_error", err);
if (err instanceof AxiosError) {
if (![401, 400].includes(err.status as number))
throw new Error(err as any);
}
}
throw new Error("Unable to create mix");
};
updateMix = async (mix: MixModel): Promise<MixModel> => {
try {
const result = await this._client.patch("/mix", mix);
if (result?.status === 201) {
return result.data;
}
} catch (err) {
console.log("authService", "getUser_error", err);
if (err instanceof AxiosError) {
if (![401, 400].includes(err.status as number)) {
throw new Error(err as any);
}
}
}
throw new Error("Unable to create mix");
};
addLike = async (mix: MixModel): Promise<boolean> => {
const result = await this._client.post(`/mix/addlike?mixId=${mix.id}`);
return result.status === 200;
};
deleteMix = async (mix: MixModel): Promise<boolean> => {
try {
const result = await this._client.delete(`/mix?id=${mix.id}`);
return result.status === 200;
} catch (err) {
console.log("authService", "getUser_error", err);
if (err instanceof AxiosError) {
if (![401, 400].includes(err.status as number)) {
throw new Error(err as any);
}
}
}
throw new Error("Unable to create mix");
};
getMixAudioUrl = async (mix: MixModel): Promise<string> => {
try {
const result = await this._client.get(`/mix/audiourl?id=${mix.id}`);
return result.data;
} catch (err) {
console.log("authService", "getUser_error", err);
if (err instanceof AxiosError) {
if (![401, 400].includes(err.status as number)) {
throw new Error(err as any);
}
}
}
throw new Error("Unable to get mix audio url");
};
}
export default MixService;

View File

@@ -0,0 +1,54 @@
import type { UserModel } from "@/lib/models";
import ApiService from "./api-service";
import logger from "@/lib/logger";
class ProfileService extends ApiService {
/**
* Get the currently logged in user's profile
*/
getProfile = async (): Promise<UserModel | undefined> => {
try {
const results = await this._client.get("/profile/me");
if (results.status === 200) {
return results.data;
}
} catch (err) {
logger.error("profile-service.ts", "Unable to get user's profile.", err);
}
return undefined;
};
getProfileBySlug = async (slug: string): Promise<UserModel | undefined> => {
try {
const results = await this._client.get(`/profile?slug=${slug}`);
if (results.status === 200) {
return results.data;
}
} catch {}
return undefined;
};
toggleFollow = async (slug: string): Promise<boolean> => {
const result = await this._client.post(
`/profile/togglefollow?slug=${slug}`,
);
return result.status === 200;
};
updateProfile = async (
profile: UserModel,
): Promise<UserModel | undefined> => {
try {
const result = await this._client.post(
`/profile`,
profile,
this.jsonConfig,
);
return result.data as UserModel;
} catch (err) {}
return undefined;
};
}
export default ProfileService;

View File

@@ -1,3 +0,0 @@
import mixService from "./mix-service";
export { mixService };

View File

@@ -1,18 +0,0 @@
import { db } from "@/server/db";
const mixService = {
getMixAudioUrl: async (mixId: string) => {
// const mixQuery = await db.query.mixes.findFirst({
// where: (mixes, { eq }) => eq(mixes.id, mixId),
// });
// if (!mixQuery) {
// throw new Error("Unable to find mix");
// }
// return `https://argle.bargle/${mixQuery.audioUrl}`;
},
};
export default mixService;

View File

@@ -1,68 +0,0 @@
import { type AdapterUser, type Adapter } from "next-auth/adapters";
export default function AspNetIdentityAdapter(baseUrl: string): Adapter {
return {
async createUser(user: Omit<AdapterUser, "id">): Promise<AdapterUser> {
const url = `${baseUrl}/account/register`;
const result = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userName: user.username,
password: "password",
confirmPassword: "password",
displayName: user.name,
email: user.email,
}),
});
if (result.status === 200) {
return result.json();
} else if (result.status === 400) {
console.log("authService", "registerUser", result);
}
throw Error(`Unable to create user: result ${result.status}`);
},
async getUser(id) {
throw Error(`Not implemented`);
},
async getUserByEmail(email) {
throw Error(`Not implemented`);
},
async getUserByAccount({ providerAccountId, provider }) {
throw Error(`Not implemented`);
},
async updateUser(user) {
throw Error(`Not implemented`);
},
async deleteUser(userId) {
throw Error(`Not implemented`);
},
async linkAccount(account) {
throw Error(`Not implemented`);
},
async unlinkAccount({ providerAccountId, provider }) {
throw Error(`Not implemented`);
},
async createSession({ sessionToken, userId, expires }) {
throw Error(`Not implemented`);
},
async getSessionAndUser(sessionToken) {
throw Error(`Not implemented`);
},
async updateSession({ sessionToken }) {
throw Error(`Not implemented`);
},
async deleteSession(sessionToken) {
throw Error(`Not implemented`);
},
async createVerificationToken({ identifier, expires, token }) {
throw Error(`Not implemented`);
},
async useVerificationToken({ identifier, token }) {
throw Error(`Not implemented`);
},
};
}

View File

@@ -0,0 +1,110 @@
import { type AuthTokenModel } from "@/lib/models";
import type { Session, AuthOptions } from "next-auth";
import AuthService from "../api/auth-service";
import jwt_decode, { type JwtPayload } from "jwt-decode";
import CredentialsProvider from "next-auth/providers/credentials";
import ProfileService from "../api/profile-service";
import { type JWT } from "next-auth/jwt";
import logger from "@/lib/logger";
export const authOptions: AuthOptions = {
secret: process.env.NEXTAUTH_SECRET,
debug: process.env.NODE_ENV === "development",
session: {
maxAge: 30 * 24 * 60 * 60, //30 days
updateAge: 24 * 60 * 60, // 24 hours
},
pages: {
signIn: "/auth/login",
newUser: "/auth/register",
signOut: "/",
},
providers: [
CredentialsProvider({
name: "Email and Password",
credentials: {
username: {
label: "Username",
type: "text",
placeholder: "Username or email address",
},
password: {
label: "Password",
type: "password",
placeholder: "Password",
},
},
authorize: async (credentials, _req): Promise<any> => {
logger.info({ authorize: "Authorizing" });
try {
if (!credentials) {
return null;
}
const token = await new AuthService().getAuthToken(
credentials.username,
credentials.password,
);
if (!token) {
return null;
}
const decodedToken = jwt_decode<JwtPayload & AuthTokenModel>(
token.access_token,
);
if (decodedToken && decodedToken.sub) {
// const session: Session = {
// id: decodedToken.sub as string,
// accessToken: token.access_token,
// expires: `${token.expires_in}`,
// user: {
// id: decodedToken.id,
// email: decodedToken.email,
// username: decodedToken.name,
// name: decodedToken.displayName,
// slug: decodedToken.slug,
// profileImage: decodedToken.profileImage,
// accessToken: token.access_token,
// },
// };
const r = {
id: decodedToken.id,
email: decodedToken.email,
username: decodedToken.name,
name: decodedToken.displayName,
slug: decodedToken.slug,
profileImage: decodedToken.profileImage,
accessToken: token.access_token,
};
logger.debug("config", "authorize_returns", r);
return Promise.resolve(r);
}
} catch (err) {
logger.error(`Error authorizing: ${err}`);
}
return null;
},
}),
],
callbacks: {
async session({ session, token }: { session: Session; token: JWT }) {
const profile = await new ProfileService(
session.accessToken,
).getProfile();
if (!profile) {
return Promise.resolve(session);
}
session.user.profile = profile;
logger.debug("config", "callback_session_Returns", session);
return session;
},
jwt: async ({ user, token }): Promise<JWT> => {
if (user) {
token.user = user;
}
logger.debug("config", "callback_jwt_Returns", token);
return Promise.resolve(token);
},
},
};

View File

@@ -0,0 +1,39 @@
import { type IncomingMessage } from "http";
export const getCookieWithName = (name: string, cookieString: string) => {
const cookieArray = cookieString.split(";");
const cookie = cookieArray.find((x) => x.includes(name));
if (!cookie) return null;
const value = cookie.split("=")[1];
return value ? decodeURIComponent(value) : undefined;
};
export const getCookieFromMiddleware = (name: string, req: any) => {
const NextRequestMetaSymbol = Reflect.ownKeys(req).find(
(key) => key.toString() === "Symbol(NextRequestMeta)",
) as string;
if (!NextRequestMetaSymbol) {
return null;
}
const NextRequestMeta = req[NextRequestMetaSymbol];
const cookieString = NextRequestMeta._nextMiddlewareCookie
.toString()
.replace("Path=/", "");
const cookieArray = cookieString.split(";");
const cookie = cookieArray.find((x: string | string[]) => x.includes(name));
const value = cookie?.split("=")[1];
return decodeURIComponent(value.trim());
};
export const getCookieFromRequest = (name: string, req: IncomingMessage) => {
const cookieString = req.headers.cookie as string;
const cookieArray = cookieString?.split(";");
const cookie = cookieArray?.find((x) => x.includes(name));
if (!cookie) {
console.log("cookie not found in the headers, trying the middleware");
return getCookieFromMiddleware(name, req);
}
const value = cookie?.split("=")[1];
return value ? decodeURIComponent(value?.trim()) : undefined;
};

View File

@@ -1,68 +0,0 @@
/**
* This is the client-side entrypoint for your tRPC API. It is used to create the `api` object which
* contains the Next.js App-wrapper, as well as your type-safe React Query hooks.
*
* We also create a few inference helpers for input and output types.
*/
import { httpBatchLink, loggerLink } from "@trpc/client";
import { createTRPCNext } from "@trpc/next";
import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
import superjson from "superjson";
import { type AppRouter } from "@/server/api/root";
const getBaseUrl = () => {
if (typeof window !== "undefined") return ""; // browser should use relative url
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
};
/** A set of type-safe react-query hooks for your tRPC API. */
export const api = createTRPCNext<AppRouter>({
config() {
return {
/**
* Transformer used for data de-serialization from the server.
*
* @see https://trpc.io/docs/data-transformers
*/
transformer: superjson,
/**
* Links used to determine request flow from client to server.
*
* @see https://trpc.io/docs/links
*/
links: [
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === "development" ||
(opts.direction === "down" && opts.result instanceof Error),
}),
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
],
};
},
/**
* Whether tRPC should await queries when server rendering pages.
*
* @see https://trpc.io/docs/nextjs#ssr-boolean-default-false
*/
ssr: false,
});
/**
* Inference helper for inputs.
*
* @example type HelloInput = RouterInputs['example']['hello']
*/
export type RouterInputs = inferRouterInputs<AppRouter>;
/**
* Inference helper for outputs.
*
* @example type HelloOutput = RouterOutputs['example']['hello']
*/
export type RouterOutputs = inferRouterOutputs<AppRouter>;

23
src/types/next-auth.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import NextAuth, { type DefaultSession } from "next-auth";
import type { UserModel } from "@/lib/models";
declare module "next-auth" {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/
interface Session {
id: string;
accessToken: string;
user: User & DefaultSession["user"];
}
interface User {
username: string;
slug: string;
email: string;
name: string | null;
profileImage: string | null;
profile?: UserModel;
}
}

View File

@@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2017", "target": "es2017",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"skipLibCheck": true, "skipLibCheck": true,
@@ -21,10 +17,9 @@
"incremental": true, "incremental": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"baseUrl": ".", "baseUrl": ".",
"typeRoots": ["src/types"],
"paths": { "paths": {
"@/*": [ "@/*": ["./src/*"]
"./src/*"
]
}, },
"plugins": [ "plugins": [
{ {
@@ -41,7 +36,5 @@
"**/*.mjs", "**/*.mjs",
".next/types/**/*.ts" ".next/types/**/*.ts"
], ],
"exclude": [ "exclude": ["node_modules"]
"node_modules"
]
} }

20
types/next-auth.d.ts vendored
View File

@@ -1,20 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import NextAuth from "next-auth";
declare module "next-auth" {
interface Session {
id: string;
user: User;
}
interface User {
id: string;
username: string;
email: string;
name: string | null;
bio: string | null;
profileImage: string | null;
headerImage: string | null;
urls: string[] | undefined;
}
}