mirror of
https://github.com/fergalmoran/mixyboos.git
synced 2025-12-22 09:41:39 +00:00
Gonna try something dangerous
This commit is contained in:
50
package.json
50
package.json
@@ -5,21 +5,18 @@
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"devq": "concurrently 'next dev' 'quirrel'",
|
||||
"ssl": "NODE_ENV=development node ./server.js",
|
||||
"sslq": "concurrently 'NODE_ENV=development node ./server.js' 'quirrel'",
|
||||
"quirrel": "quirrel",
|
||||
"lint": "next lint",
|
||||
"start": "next start",
|
||||
"test": "export $(xargs < .env) && jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.16",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-progress": "^1.0.3",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"@radix-ui/react-tooltip": "^1.0.6",
|
||||
"@sentry/nextjs": "^7.62.0",
|
||||
"@sentry/nextjs": "^7.64.0",
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@t3-oss/env-nextjs": "^0.6.0",
|
||||
"@tanstack/react-query": "^4.32.6",
|
||||
@@ -33,29 +30,28 @@
|
||||
"import-local": "^3.1.0",
|
||||
"install": "^0.13.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"next": "13.4.13",
|
||||
"next-auth": "^4.22.5",
|
||||
"pg": "^8.11.2",
|
||||
"next": "13.4.17",
|
||||
"next-auth": "^4.23.1",
|
||||
"pg": "^8.11.3",
|
||||
"pino": "^8.15.0",
|
||||
"pino-logflare": "^0.4.0",
|
||||
"postgres": "^3.3.5",
|
||||
"quirrel": "^1.14.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^4.10.1",
|
||||
"superjson": "1.13.1",
|
||||
"yup": "^1.2.0",
|
||||
"zod": "^3.21.4",
|
||||
"zod": "^3.22.1",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@azure/identity": "^3.2.4",
|
||||
"@azure/identity": "^3.3.0",
|
||||
"@azure/storage-blob": "^12.15.0",
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@hookform/resolvers": "^3.2.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-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
@@ -65,37 +61,37 @@
|
||||
"@types/eslint": "^8.44.2",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/node": "^20.4.9",
|
||||
"@types/node": "^20.5.0",
|
||||
"@types/pg": "^8.10.2",
|
||||
"@types/react": "^18.2.19",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/superagent": "^4.1.18",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
||||
"@typescript-eslint/parser": "^6.3.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.0",
|
||||
"@typescript-eslint/parser": "^6.4.0",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"concurrently": "^8.2.0",
|
||||
"dayjs": "^1.11.9",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-config-next": "^13.4.13",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-config-next": "^13.4.17",
|
||||
"hls.js": "^1.4.10",
|
||||
"jest": "^29.6.2",
|
||||
"lucide-react": "^0.265.0",
|
||||
"lucide-react": "^0.268.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"node-mocks-http": "^1.12.2",
|
||||
"postcss": "^8.4.27",
|
||||
"prettier": "3.0.1",
|
||||
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||
"node-mocks-http": "^1.13.0",
|
||||
"pino-pretty": "^10.2.0",
|
||||
"postcss": "^8.4.28",
|
||||
"prettier": "3.0.2",
|
||||
"prettier-plugin-tailwindcss": "^0.5.3",
|
||||
"pusher": "^5.1.3",
|
||||
"pusher-js": "^8.3.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"recharts": "^2.7.3",
|
||||
"retry": "^0.13.1",
|
||||
"superagent": "^8.0.9",
|
||||
"superagent": "^8.1.2",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tailwindcss-animate": "^1.0.6",
|
||||
@@ -103,7 +99,7 @@
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.1.6",
|
||||
"uuid": "^9.0.0",
|
||||
"vitest": "^0.34.1"
|
||||
"vitest": "^0.34.2"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.12.2"
|
||||
|
||||
2892
pnpm-lock.yaml
generated
2892
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import Loading from "@/lib/components/widgets/Loading";
|
||||
import { api } from "@/lib/utils/api";
|
||||
import { type NextPage } from "next";
|
||||
import React from "react";
|
||||
import ProfileEditForm from "../../components/profile-edit-form";
|
||||
|
||||
const ProfileSettingsPage: NextPage = () => {
|
||||
const { data: profile, status } = api.user.getProfileForSettings.useQuery();
|
||||
|
||||
if (status === "loading") {
|
||||
return <Loading />;
|
||||
}
|
||||
return profile ? (
|
||||
<ProfileEditForm profile={profile} />
|
||||
) : (
|
||||
<div>Nothing here bai!</div>
|
||||
);
|
||||
// if (status === "loading") {
|
||||
// return <Loading />;
|
||||
// }
|
||||
// return profile ? (
|
||||
// <ProfileEditForm profile={profile} />
|
||||
// ) : (
|
||||
// <div>Nothing here bai!</div>
|
||||
// );
|
||||
};
|
||||
|
||||
export default ProfileSettingsPage;
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
"use client";
|
||||
import LargeAudioPlayer from "@/components/widgets/audio/large-audio-player";
|
||||
import React from "react";
|
||||
import { api } from "@/lib/utils/api";
|
||||
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: { username: string; mixSlug: string };
|
||||
}) {
|
||||
const mixQuery = api.mix.getByUserAndSlug.useQuery({
|
||||
username: params.username,
|
||||
mixSlug: params.mixSlug,
|
||||
});
|
||||
const mix = mixQuery?.data;
|
||||
const mix = await new MixService().getByUserAndSlug(
|
||||
params.username,
|
||||
params.mixSlug,
|
||||
);
|
||||
return (
|
||||
<div className="container p-5">
|
||||
{mix ? (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { authOptions } from "@/lib/services/api/config";
|
||||
import { authOptions } from "@/lib/services/auth/config";
|
||||
import NextAuth from "next-auth";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export function GET() {
|
||||
return NextResponse.json({ debug: "deez nuts" });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export function GET() {
|
||||
return NextResponse.json({ ping: "pong" });
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import Link from "next/link";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import logger from "@/lib/logger";
|
||||
|
||||
const LoginPage = () => {
|
||||
const [loginError, setLoginError] = React.useState(false);
|
||||
@@ -45,10 +46,10 @@ const LoginPage = () => {
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
// Do something with the form values.
|
||||
// ✅ This will be type-safe and validated.
|
||||
console.log(values);
|
||||
logger.debug(values);
|
||||
setLoginError(false);
|
||||
signIn("credentials", {
|
||||
email: values.usernameOrEmail,
|
||||
username: values.usernameOrEmail,
|
||||
password: values.password,
|
||||
callbackUrl:
|
||||
searchParams?.get("callbackUrl") ||
|
||||
@@ -57,14 +58,16 @@ const LoginPage = () => {
|
||||
redirect: false,
|
||||
})
|
||||
.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") || "/");
|
||||
} else {
|
||||
setLoginError(true);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("login", "handleLogin", err);
|
||||
logger.error("login", "handleLogin", err);
|
||||
setLoginError(true);
|
||||
});
|
||||
}
|
||||
@@ -109,6 +112,13 @@ const LoginPage = () => {
|
||||
</button>
|
||||
</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 onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
@@ -155,15 +165,7 @@ const LoginPage = () => {
|
||||
</div>
|
||||
</Button>
|
||||
</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">
|
||||
Not registered?
|
||||
<Link
|
||||
|
||||
@@ -19,9 +19,12 @@ import { Icons } from "@/components/icons";
|
||||
import { notice } from "@/lib/components/notifications/toast";
|
||||
import Link from "next/link";
|
||||
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 [hasErrors, setHasErrors] = React.useState(false);
|
||||
const schema = z
|
||||
.object({
|
||||
username: z
|
||||
@@ -42,33 +45,35 @@ const LoginPage = () => {
|
||||
const form = useForm<z.infer<typeof schema>>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
const register = api.auth.signUp.useMutation({
|
||||
onSuccess: (result) => {
|
||||
console.log("page", "register_success", result);
|
||||
username: "superawesomedjperson",
|
||||
email: "fergal.moran+mixyboos@gmail.com",
|
||||
password: "secret",
|
||||
confirmPassword: "secret",
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(values: z.infer<typeof schema>) {
|
||||
// Do something with the form values.
|
||||
// ✅ This will be type-safe and validated.
|
||||
console.log(values);
|
||||
logger.debug(values);
|
||||
setHasErrors(false);
|
||||
try {
|
||||
const result = await register.mutateAsync({
|
||||
email: values.email,
|
||||
const result = await new AuthService().registerUser(
|
||||
values.email,
|
||||
values.password,
|
||||
values.confirmPassword,
|
||||
values.username,
|
||||
);
|
||||
if (result) {
|
||||
const response = await signIn("credentials", {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
callbackUrl: "/",
|
||||
redirect: true,
|
||||
});
|
||||
console.log("page", "handleRegister", result);
|
||||
if (result?.status === 201) {
|
||||
await signIn();
|
||||
logger.debug("RegisterPage", "signin_credentials", response);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("RegisterPage", "handleLogin", err);
|
||||
logger.error("RegisterPage", "handleLogin", err);
|
||||
setHasErrors(true);
|
||||
}
|
||||
}
|
||||
return (
|
||||
@@ -114,6 +119,22 @@ const LoginPage = () => {
|
||||
<div className="border-sm mt-4 text-center shadow-lg">
|
||||
or create a new account
|
||||
</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 onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
|
||||
@@ -1,35 +1,10 @@
|
||||
"use client";
|
||||
|
||||
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 Session from "@/lib/components/debug/Session";
|
||||
import { getServerSession } from "next-auth";
|
||||
import React from "react";
|
||||
|
||||
const IndexPage = () => {
|
||||
const session = useSession();
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
const session = getServerSession();
|
||||
return <Session serverSession={session} />;
|
||||
};
|
||||
|
||||
export default IndexPage;
|
||||
|
||||
@@ -2,7 +2,7 @@ import HeroPage from "@/lib/components/pages/HeroPage";
|
||||
import React from "react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { authOptions } from "@/lib/services/api/config";
|
||||
import { authOptions } from "@/lib/services/auth/config";
|
||||
|
||||
const Home = async () => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
@@ -1,80 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts";
|
||||
import React from "react";
|
||||
|
||||
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>
|
||||
);z
|
||||
const PlaysGraph: React.FC = () => {
|
||||
return <div>PlaysGraph</div>;
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
23
src/lib/components/debug/Session.tsx
Normal file
23
src/lib/components/debug/Session.tsx
Normal 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;
|
||||
34
src/lib/components/debug/SessionPrinter.tsx
Normal file
34
src/lib/components/debug/SessionPrinter.tsx
Normal 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;
|
||||
@@ -1,31 +1,14 @@
|
||||
import pino from "pino";
|
||||
import { logflarePinoVercel } from "pino-logflare";
|
||||
|
||||
// create pino-logflare console stream for serverless functions and send function for browser logs
|
||||
// Browser logs are going to: https://logflare.app/sources/13989
|
||||
// Vercel log drain was setup to send logs here: https://logflare.app/sources/13830
|
||||
|
||||
const { stream, send } = logflarePinoVercel({
|
||||
apiKey: "MK3qgU_-pwHQ",
|
||||
sourceToken: "2c94c62f-012d-4891-8ef5-38103089e4af",
|
||||
const mixin = {
|
||||
appName: "MixyBoos",
|
||||
target: 'pino-pretty'
|
||||
};
|
||||
// create pino logger
|
||||
const logger = pino({
|
||||
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;
|
||||
|
||||
@@ -5,5 +5,12 @@ type AuthTokenModel = {
|
||||
expires_in: number;
|
||||
token_type: string;
|
||||
expiration_date: string;
|
||||
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
slug: string;
|
||||
profileImage: string;
|
||||
};
|
||||
export default AuthTokenModel;
|
||||
|
||||
@@ -35,9 +35,7 @@ class ApiService {
|
||||
const session = await getSession();
|
||||
if (session || this._token) {
|
||||
config.headers = {
|
||||
Authorization: `Bearer ${
|
||||
session ? session.user.accessToken : this._token
|
||||
}`,
|
||||
Authorization: `Bearer ${session ? session.accessToken : this._token}`,
|
||||
Accept: "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AxiosError } from "axios";
|
||||
import ApiService from "./api-service";
|
||||
import { type AuthTokenModel, UserModel } from "@/lib/models";
|
||||
import type { AuthTokenModel, UserModel } from "@/lib/models";
|
||||
import logger from "@/lib/logger";
|
||||
|
||||
class AuthService extends ApiService {
|
||||
@@ -78,15 +78,15 @@ class AuthService extends ApiService {
|
||||
return Promise.reject("Unable to log in");
|
||||
};
|
||||
registerUser = async (
|
||||
userName: string,
|
||||
email: string,
|
||||
password: string,
|
||||
confirmPassword: string,
|
||||
displayName: string,
|
||||
username: string = "",
|
||||
): Promise<boolean> => {
|
||||
const url = "/account/register";
|
||||
const result = await this._client.post(
|
||||
url,
|
||||
{ userName, password, confirmPassword, displayName },
|
||||
{ username: email, password, confirmPassword, displayName: username },
|
||||
this.jsonConfig,
|
||||
);
|
||||
|
||||
@@ -97,6 +97,7 @@ class AuthService extends ApiService {
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
loginUser = async (
|
||||
username: string,
|
||||
password: string,
|
||||
@@ -106,7 +107,6 @@ class AuthService extends ApiService {
|
||||
const params = new URLSearchParams();
|
||||
params.append("username", username);
|
||||
params.append("password", password);
|
||||
UserModel;
|
||||
params.append("grant_type", process.env.AUTH_GRANT_TYPE as string);
|
||||
params.append("scope", process.env.AUTH_SCOPE as string);
|
||||
params.append("client_id", process.env.AUTH_CLIENT_ID as string);
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
143
src/lib/services/api/mix-service.ts
Normal file
143
src/lib/services/api/mix-service.ts
Normal 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;
|
||||
54
src/lib/services/api/profile-service.ts
Normal file
54
src/lib/services/api/profile-service.ts
Normal 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;
|
||||
@@ -1,3 +0,0 @@
|
||||
import mixService from "./mix-service";
|
||||
|
||||
export { mixService };
|
||||
@@ -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;
|
||||
@@ -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`);
|
||||
},
|
||||
};
|
||||
}
|
||||
110
src/lib/services/auth/config.ts
Normal file
110
src/lib/services/auth/config.ts
Normal 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);
|
||||
},
|
||||
},
|
||||
};
|
||||
39
src/lib/services/auth/cookie.ts
Normal file
39
src/lib/services/auth/cookie.ts
Normal 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;
|
||||
};
|
||||
@@ -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
23
src/types/next-auth.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"skipLibCheck": true,
|
||||
@@ -21,10 +17,9 @@
|
||||
"incremental": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"baseUrl": ".",
|
||||
"typeRoots": ["src/types"],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
@@ -41,7 +36,5 @@
|
||||
"**/*.mjs",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
20
types/next-auth.d.ts
vendored
20
types/next-auth.d.ts
vendored
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user