Added sign in action

This commit is contained in:
Sianida26 2024-01-15 01:39:29 +07:00
parent 9ad195ad78
commit d05fa7e27d
19 changed files with 167 additions and 371 deletions

2
.env
View File

@ -1,3 +1,3 @@
DATABASE_URL=mysql://root:root@localhost:3306/dashboard_template
NEXTAUTH_SECRET=
JWT_SECRET=

View File

@ -21,9 +21,10 @@
"@trpc/react-query": "^10.45.0",
"@trpc/server": "^10.45.0",
"@types/bcrypt": "^5.0.2",
"@types/jsonwebtoken": "^9.0.5",
"bcrypt": "^5.1.1",
"jsonwebtoken": "^9.0.2",
"next": "14.0.4",
"next-auth": "beta",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"superjson": "^2.2.1",

View File

@ -41,15 +41,18 @@ dependencies:
'@types/bcrypt':
specifier: ^5.0.2
version: 5.0.2
'@types/jsonwebtoken':
specifier: ^9.0.5
version: 9.0.5
bcrypt:
specifier: ^5.1.1
version: 5.1.1
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.2
next:
specifier: 14.0.4
version: 14.0.4(react-dom@18.2.0)(react@18.2.0)
next-auth:
specifier: beta
version: 5.0.0-beta.4(next@14.0.4)(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
@ -113,22 +116,6 @@ packages:
engines: {node: '>=10'}
dev: true
/@auth/core@0.18.4:
resolution: {integrity: sha512-GsNhsP1xE/3FoNS3dVkPjqRljLNJ4iyL2OLv3klQGNvw3bMpROFcK4lqhx7+pPHiamnVaYt2vg1xbB+lsNaevg==}
peerDependencies:
nodemailer: ^6.8.0
peerDependenciesMeta:
nodemailer:
optional: true
dependencies:
'@panva/hkdf': 1.1.1
cookie: 0.6.0
jose: 5.2.0
oauth4webapi: 2.4.3
preact: 10.11.3
preact-render-to-string: 5.2.3(preact@10.11.3)
dev: false
/@auth/core@0.20.0:
resolution: {integrity: sha512-04lQH58H5d/9xQ63MOTDTOC7sXWYlr/RhJ97wfFLXzll7nYyCKbkrT3ZMdzdLC5O+qt90sQDK85TAtLlcZ2WBg==}
peerDependencies:
@ -646,6 +633,12 @@ packages:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true
/@types/jsonwebtoken@9.0.5:
resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==}
dependencies:
'@types/node': 20.10.6
dev: false
/@types/node@20.10.6:
resolution: {integrity: sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==}
dependencies:
@ -1009,6 +1002,10 @@ packages:
update-browserslist-db: 1.0.13(browserslist@4.22.2)
dev: true
/buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
dev: false
/busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
@ -1231,6 +1228,12 @@ packages:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
dev: true
/ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
dependencies:
safe-buffer: 5.2.1
dev: false
/electron-to-chromium@1.4.623:
resolution: {integrity: sha512-lKoz10iCYlP1WtRYdh5MvocQPWVRoI7ysp6qf18bmeBgR8abE6+I2CsfyNKztRDZvhdWc+krKT6wS7Neg8sw3A==}
dev: true
@ -2203,6 +2206,22 @@ packages:
minimist: 1.2.8
dev: true
/jsonwebtoken@9.0.2:
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
engines: {node: '>=12', npm: '>=6'}
dependencies:
jws: 3.2.2
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.5.4
dev: false
/jsx-ast-utils@3.3.5:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
@ -2213,6 +2232,21 @@ packages:
object.values: 1.1.7
dev: true
/jwa@1.4.1:
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
dev: false
/jws@3.2.2:
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
dependencies:
jwa: 1.4.1
safe-buffer: 5.2.1
dev: false
/keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
dependencies:
@ -2264,10 +2298,38 @@ packages:
p-locate: 5.0.0
dev: true
/lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
dev: false
/lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
dev: false
/lodash.isinteger@4.0.4:
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
dev: false
/lodash.isnumber@3.0.3:
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
dev: false
/lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
dev: false
/lodash.isstring@4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
dev: false
/lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true
/lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
dev: false
/loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@ -2357,7 +2419,6 @@ packages:
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: true
/mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
@ -2376,21 +2437,6 @@ packages:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true
/next-auth@5.0.0-beta.4(next@14.0.4)(react@18.2.0):
resolution: {integrity: sha512-vgocjvwPA8gxd/zrIP/vr9lJ/HeNe+C56lPP1D3sdyenHt8KncQV6ro7q0xCsDp1fcOKx7WAWVZH5o8aMxDzgw==}
peerDependencies:
next: ^14
nodemailer: ^6.6.5
react: ^18.2.0
peerDependenciesMeta:
nodemailer:
optional: true
dependencies:
'@auth/core': 0.18.4
next: 14.0.4(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
dev: false
/next@14.0.4(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==}
engines: {node: '>=18.17.0'}

View File

@ -1,6 +1,6 @@
"use client";
import { signIn } from "next-auth/react";
import signIn from "@/features/auth/actions/signIn";
import {
Paper,
PasswordInput,
@ -12,68 +12,34 @@ import {
Button,
Alert,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import React, { useState } from "react";
import React from "react";
import { useFormState } from "react-dom";
/**
* Type definition for login form values.
*/
interface LoginFormType {
email: string;
password: string;
}
/**
* LoginPage component: Renders a login form allowing users to authenticate using their credentials.
* Utilizes Mantine for UI components and Next-Auth for authentication handling.
*
* @returns React functional component representing the login page.
*/
export default function LoginPage() {
const form = useForm<LoginFormType>({
initialValues: {
email: "",
password: "",
const initialState = {
errors: {
message: "",
},
});
const [errorMessage, setErrorMessage] = useState("");
/**
* Handles form submission by calling Next-Auth signIn function with credentials.
*
* @param values - Object containing email and password entered by the user.
*/
const handleFormSubmit = async (values: LoginFormType) => {
try {
await signIn("credentials", {
email: values.email,
password: values.password,
callbackUrl: "/",
redirect: false,
});
} catch (e) {
// TODO: Handle proper error message
setErrorMessage("Email/Password does not match");
}
};
export default function LoginPage() {
const [state, formAction] = useFormState(signIn, initialState);
return (
<div className="w-screen h-screen flex items-center justify-center">
<Paper radius="md" p="xl" withBorder w={400}>
<Text size="lg" fw={500} mb={30}>
Welcome
</Text>
<form onSubmit={form.onSubmit(handleFormSubmit)}>
<form action={formAction}>
<Stack>
{errorMessage ? (
{state.errors.message ? (
<Alert
variant="filled"
color="pink"
title=""
// icon={icon}
>
{errorMessage}
{state.errors.message}
</Alert>
) : null}
<TextInput
@ -81,14 +47,12 @@ export default function LoginPage() {
placeholder="Enter your email"
name="email"
autoComplete="email"
{...form.getInputProps("email")}
/>
<PasswordInput
label="Password"
placeholder="Your password"
name="password"
autoComplete="password"
{...form.getInputProps("password")}
/>
</Stack>

View File

@ -1,7 +1,5 @@
"use client";
import { auth } from "@/features/auth";
import { signIn } from "next-auth/react";
import {
Paper,
PasswordInput,
@ -14,7 +12,6 @@ import {
} from "@mantine/core";
import { useForm } from "@mantine/form";
import React, { useEffect } from "react";
import { api } from "@/trpc/utils";
export interface RegisterFormSchema {
email: string,
@ -40,30 +37,13 @@ export default function RegisterPage() {
}
});
const registerMutation = api.auth.register.useMutation({
onSuccess: async () => {
console.log("success. signing in")
await signIn("credentials", {
email: form.values.email,
password: form.values.password,
callbackUrl: "/dashboard"
})
console.log("signed in")
}
})
const handleFormSubmit = (values: RegisterFormSchema) => {
// await
registerMutation.mutate(values)
}
return (
<div className="w-screen h-screen flex items-center justify-center">
<Paper radius="md" p="xl" withBorder w={400}>
<Text size="lg" fw={500} mb={30}>
Register
</Text>
<form onSubmit={form.onSubmit(handleFormSubmit)}>
<form onSubmit={form.onSubmit(() => {})}>
<Stack>
<TextInput
label="Name"

View File

@ -1 +0,0 @@
export {GET, POST} from "@/features/auth"

View File

@ -1,12 +0,0 @@
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/trpc/routes/_app';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => ({ })
});
export { handler as GET, handler as POST };

View File

@ -5,7 +5,6 @@ import "./globals.css"
import '@mantine/core/styles.css';
import { ColorSchemeScript, MantineProvider } from '@mantine/core';
import { TrpcProvider } from '@/trpc/TrpcProvider';
const inter = Inter({ subsets: ['latin'] })
@ -26,9 +25,7 @@ export default function RootLayout({
</head>
<body className={inter.className}>
<MantineProvider>
<TrpcProvider>
{children}
</TrpcProvider>
</MantineProvider>
</body>
</html>

View File

@ -4,7 +4,8 @@ export enum AuthErrorCode {
EMAIL_NOT_FOUND = "EMAIL_NOT_FOUND",
EMPTY_USER_HASH = "EMPTY_USER_HASH",
INVALID_CREDENTIALS = "INVALID_CREDENTIALS",
USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS"
JWT_SECRET_EMPTY = "JWT_SECRET_NOT_EMPTY",
USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS",
}
export default class AuthError extends BaseError {

View File

@ -1,7 +1,10 @@
"use server"
import prisma from "@/db";
import { User } from "@prisma/client";
import AuthError, { AuthErrorCode } from "../AuthError";
import { comparePassword } from "../authUtils";
import { comparePassword, createJwtToken } from "../authUtils";
import { cookies } from "next/headers";
import { redirect } from 'next/navigation';
/**
* Validates the user by their email and password.
@ -13,10 +16,17 @@ import { comparePassword } from "../authUtils";
* @returns The authenticated user object.
* @throws {AuthError} - EMAIL_NOT_FOUND if no user is found, INVALID_CREDENTIALS if the password doesn't match, or other auth-related errors.
*/
export default async function signIn(email: string, password: string): Promise<User> {
export default async function signIn(prevState: any, rawFormData: FormData) {
//TODO: Add Throttling
try {
const formData = {
email: rawFormData.get("email") as string,
password: rawFormData.get("password") as string
}
// Retrieve user from the database by email
const user = await prisma.user.findUnique({
where: { email }
where: { email: formData.email }
});
// Throw if user not found
@ -27,8 +37,34 @@ export default async function signIn(email: string, password: string): Promise<U
if (!user.passwordHash) throw new AuthError(AuthErrorCode.EMPTY_USER_HASH, 500);
// Compare the provided password with the user's stored password hash
const isMatch = await comparePassword(password, user.passwordHash);
const isMatch = await comparePassword(formData.password, user.passwordHash);
if (!isMatch) throw new AuthError(AuthErrorCode.INVALID_CREDENTIALS, 401);
return user;
//Set cookie
//TODO: Auth: Add expiry
const token = createJwtToken({id: user.id});
cookies().set("token",token);
redirect("/dashboard");
} catch (e: unknown){
if (e instanceof AuthError){
if ([
AuthErrorCode.EMAIL_NOT_FOUND, AuthErrorCode.INVALID_CREDENTIALS
]) {
return {
errors: {
message: "Email/Password combination is not match. Please try again"
}
}
}
}
return {
errors: {
message: "There's something wrong happened on the server. Please try again or contact administrator"
}
}
}
}

View File

@ -3,6 +3,8 @@ import { User } from "@prisma/client";
import * as bcrypt from "bcrypt";
import AuthError, { AuthErrorCode } from "./AuthError";
import authConfig from "@/config/auth";
import jwt from "jsonwebtoken"
import UserClaims from "./types/UserClaims";
/**
* Hashes a plain text password using bcrypt.
@ -24,3 +26,10 @@ export async function hashPassword(password: string): Promise<string> {
export async function comparePassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
export function createJwtToken(userclaims: UserClaims, options?: jwt.SignOptions){
const secret = process.env.JWT_SECRET;
if (!secret) throw new AuthError(AuthErrorCode.JWT_SECRET_EMPTY);
const token = jwt.sign(userclaims, secret, options);
return token;
}

View File

@ -1,50 +0,0 @@
import NextAuth from "next-auth";
import emailPasswordProvider from "./providers/emailPasswordProvider";
import prisma from "@/db";
import { PrismaAdapter } from "@auth/prisma-adapter";
const nextAuth = NextAuth({
adapter: PrismaAdapter(prisma),
session: {
strategy: "jwt"
},
providers:[
emailPasswordProvider
],
callbacks: {
// signIn: ({ user, account, profile, email, credentials}) => {
// console.log("sign in callback")
// console.table({user, account, profile, email, credentials})
// return true;
// },
// session: async({session, user, token}) => {
// if (session.user){
// session.user.id = token.userId as string;
// }
// return session;
// },
// jwt: async ({ token, user, account, profile }) => {
// if(account && account.type === "credentials") {
// token.userId = account.providerAccountId; // this is Id that coming from authorize() callback
// }
// return token
// },
// redirect: async ({url, baseUrl}) => {
// console.log("redirect callback called")
// console.table({url, baseUrl})
// return url
// }
},
pages: {
signIn: "/login"
}
});
export const {
signIn,
signOut,
handlers: { GET, POST},
auth
} = nextAuth;
export default nextAuth;

View File

@ -1,47 +0,0 @@
import CredentialsProvider from "next-auth/providers/credentials"
import AuthError, { AuthErrorCode } from "../AuthError";
import BaseError from "@/BaseError";
import signIn from "../actions/signIn";
/**
* Factory function to create a credential provider.
* It defines the structure of the credentials and includes an authorization function
* to validate the user's credentials.
*
* @returns A CredentialsProvider instance configured for email-password authentication.
*/
const credential = CredentialsProvider({
name: "email-password",
credentials: {
email: {
label: "Email",
type: "email",
},
password: {
label: "password",
type: "password"
}
},
authorize: async (credentials) => {
try {
// Ensure credentials are properly formatted strings
if (typeof credentials.email !== "string" || typeof credentials.password !== "string"){
throw new AuthError(AuthErrorCode.INVALID_CREDENTIALS, 401);
}
// Validate user with provided credentials
const user = await signIn(credentials.email, credentials.password);
return user;
} catch (e: unknown){
// Handle specific authentication errors, re-throw others
if (e instanceof AuthError){
// Auth invalid
if ([AuthErrorCode.EMAIL_NOT_FOUND, AuthErrorCode.EMPTY_USER_HASH, AuthErrorCode.INVALID_CREDENTIALS].includes(e.errorCode as AuthErrorCode))
return null;
}
throw e;
}
},
})
export default credential;

View File

@ -0,0 +1,7 @@
import { User } from "@prisma/client"
type UserClaims = {
id: User["id"]
}
export default UserClaims

View File

@ -1,37 +0,0 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink, getFetch, loggerLink } from "@trpc/client";
import { useState } from "react";
import superjson from "superjson";
import { api as trpc } from "@/trpc/utils";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
export const TrpcProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
// You can pass any HTTP headers you wish here
async headers() {
return {
// authorization: getAuthCookie(),
};
},
}),
],
}),
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools />
</QueryClientProvider>
</trpc.Provider>
);
};

View File

@ -1,6 +0,0 @@
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
// Base router and procedure helpers
export const router = t.router;
export const procedure = t.procedure;

View File

@ -1,21 +0,0 @@
import { z } from 'zod';
import { procedure, router } from '..';
import authRouter from './auth';
export const appRouter = router({
hello: procedure
.input(
z.object({
text: z.string(),
}),
)
.query((opts) => {
return {
greeting: `hello ${opts.input.text}`,
};
}),
auth: authRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;

View File

@ -1,46 +0,0 @@
import { z } from "zod";
import { procedure, router } from "..";
import prisma from "@/db";
import createUser from "@/features/auth/actions/createUser";
import { AuthError } from "next-auth";
import { TRPCError } from "@trpc/server";
const authRouter = router({
register: procedure
.input(
z.object({
name: z.string(),
email: z.string().email(),
password: z.string(),
passwordConfirmation: z.string(),
})
.refine(data => data.password === data.passwordConfirmation, {
message: "Password don't match",
path: ["passwordConfirmation"]
})
)
.mutation(async ({input}) => {
try {
const user = await createUser({
email: input.email,
name: input.name,
plainPassword: input.password
})
return "ok"
} catch (e: unknown) {
if (e instanceof AuthError){
throw new TRPCError({
code: "BAD_REQUEST",
message: e.message,
cause: e
})
}
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR"
})
}
})
})
export default authRouter;

View File

@ -1,25 +0,0 @@
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { AppRouter } from './routes/_app';
import { createTRPCReact } from '@trpc/react-query';
function getBaseUrl() {
if (typeof window !== 'undefined')
// browser should use relative path
return '';
if (process.env.VERCEL_URL)
// reference for vercel.com
return `https://${process.env.VERCEL_URL}`;
if (process.env.RENDER_INTERNAL_HOSTNAME)
// reference for render.com
return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;
// assume localhost
return `http://localhost:${process.env.PORT ?? 3000}`;
}
export const api = createTRPCReact<AppRouter>({
})