added oauth Route
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
NEXTAUTH_URL=http://localhost:3002
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
NEXTAUTH_SECRET=dsadsadsa
|
||||
DATABASE_URL=postgresql://persistant-data:persistant-data-pw@localhost:5432/var
|
||||
27
apps/hub/app/(auth)/layout.tsx
Normal file
27
apps/hub/app/(auth)/layout.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextPage } from 'next';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
const AuthLayout: NextPage<
|
||||
Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>
|
||||
> = ({ children }) => (
|
||||
<div
|
||||
className="hero min-h-screen"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'url(https://img.daisyui.com/images/stock/photo-1507358522600-9f71e620c44e.webp)',
|
||||
}}
|
||||
>
|
||||
<div className="hero-overlay bg-opacity-60"></div>
|
||||
<div className="hero-content text-neutral-content text-center ">
|
||||
<div className="max-w-lg">
|
||||
<div className="card bg-base-100 w-full min-w-[500px] shadow-2xl max-md:min-w-[400px]">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default AuthLayout;
|
||||
@@ -7,13 +7,12 @@ import { useForm } from 'react-hook-form';
|
||||
import { Toaster, toast } from 'react-hot-toast';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
||||
export const Login = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
type schemaType = z.infer<typeof schema>;
|
||||
@@ -28,29 +27,25 @@ export const Login = () => {
|
||||
onSubmit={form.handleSubmit(async () => {
|
||||
setIsLoading(true);
|
||||
const data = await signIn('credentials', {
|
||||
redirect: false,
|
||||
callbackUrl: '/',
|
||||
email: form.getValues('email'),
|
||||
password: form.getValues('password'),
|
||||
});
|
||||
if (!data || data.error) {
|
||||
toast.error("E-Mail / Passwort ist falsch!",
|
||||
{
|
||||
style: {
|
||||
background: 'var(--fallback-b1, oklch(var(--b1) / var(--tw-bg-opacity, 1)))',
|
||||
color: 'var(--fallback-nc, oklch(var(--nc) / var(--tw-text-opacity, 1)))'
|
||||
}
|
||||
}
|
||||
)
|
||||
toast.error('E-Mail / Passwort ist falsch!', {
|
||||
style: {
|
||||
background:
|
||||
'var(--fallback-b1, oklch(var(--b1) / var(--tw-bg-opacity, 1)))',
|
||||
color:
|
||||
'var(--fallback-nc, oklch(var(--nc) / var(--tw-text-opacity, 1)))',
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log(data);
|
||||
setIsLoading(false);
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<Toaster
|
||||
position="top-center"
|
||||
reverseOrder={false}
|
||||
/>
|
||||
<Toaster position="top-center" reverseOrder={false} />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold">Login</h1>
|
||||
<span className="text-sm font-medium">
|
||||
@@ -60,55 +55,60 @@ export const Login = () => {
|
||||
</Link>
|
||||
</span>
|
||||
<label className="input input-bordered flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
className="h-4 w-4 opacity-70"
|
||||
>
|
||||
<path d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z" />
|
||||
<path d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z" />
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
className="grow"
|
||||
{...form.register('email')}
|
||||
placeholder="Email"
|
||||
/>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
className="h-4 w-4 opacity-70"
|
||||
>
|
||||
<path d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z" />
|
||||
<path d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z" />
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
className="grow"
|
||||
{...form.register('email')}
|
||||
placeholder="Email"
|
||||
/>
|
||||
</label>
|
||||
<p className="text-error">
|
||||
{typeof form.formState.errors.email?.message === 'string'
|
||||
? form.formState.errors.email.message
|
||||
: ''}
|
||||
</p>
|
||||
<label className="input input-bordered flex items-center gap-2 mt-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
className="h-4 w-4 opacity-70"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
{...form.register('password')}
|
||||
placeholder="Passwort"
|
||||
className="grow"
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
className="h-4 w-4 opacity-70"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</label>
|
||||
</svg>
|
||||
<input
|
||||
autoComplete="current-password"
|
||||
type="password"
|
||||
{...form.register('password')}
|
||||
placeholder="Passwort"
|
||||
className="grow"
|
||||
/>
|
||||
</label>
|
||||
<div className="form-control mt-6">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
name="loginBtn"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && (
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
)}
|
||||
Login{isLoading && '...'}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
name="loginBtn"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && (
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
)}
|
||||
Login{isLoading && '...'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,23 +2,8 @@ import { Login } from './_components/Login';
|
||||
|
||||
export default async () => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="hero min-h-screen"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'url(https://img.daisyui.com/images/stock/photo-1507358522600-9f71e620c44e.webp)',
|
||||
}}
|
||||
>
|
||||
<div className="hero-overlay bg-opacity-60"></div>
|
||||
<div className="hero-content text-neutral-content text-center ">
|
||||
<div className="max-w-lg">
|
||||
<div className="card bg-base-100 w-full min-w-[500px] shadow-2xl max-md:min-w-[400px]">
|
||||
<Login />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
<>
|
||||
<Login />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ export default () => {
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
<div className="card-body">
|
||||
<h1 className="text-5xl">logging out...</h1>
|
||||
</div>
|
||||
);
|
||||
|
||||
43
apps/hub/app/(auth)/oauth/_components/Authorize.tsx
Normal file
43
apps/hub/app/(auth)/oauth/_components/Authorize.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Service } from '../page';
|
||||
import { generateToken } from './action';
|
||||
|
||||
export const Authorize = ({ service }: { service: Service }) => {
|
||||
const searchParams = useSearchParams();
|
||||
const legitimeUrl = service.approvedUrls.some((url) =>
|
||||
searchParams.get('redirect_uri')?.startsWith(url)
|
||||
);
|
||||
if (!legitimeUrl)
|
||||
return (
|
||||
<div className="card-body">
|
||||
<h1 className="text-4xl font-bold">Unerlaubter Zugriff</h1>
|
||||
<p>Du greifst von einem ncith genehmigtem Server auf diese URL zu</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<form className="card-body" onSubmit={(e) => e.preventDefault()}>
|
||||
<h1 className="text-4xl font-bold">Zugriff zulassen</h1>
|
||||
<p>
|
||||
Die Anwendung <strong>{service.name}</strong> möchte auf deine Daten
|
||||
zugreifen.
|
||||
</p>
|
||||
<div className="space-x-4">
|
||||
<button type="button" className="btn">
|
||||
Verweigern
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
onClick={async () => {
|
||||
const code = await generateToken(service);
|
||||
window.location.href = `${searchParams.get('redirect_uri')}?code=${code}`;
|
||||
}}
|
||||
>
|
||||
Zulassen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
24
apps/hub/app/(auth)/oauth/_components/action.ts
Normal file
24
apps/hub/app/(auth)/oauth/_components/action.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
'use server';
|
||||
import { getServerSession } from '../../../api/auth/[...nextauth]/auth';
|
||||
import { Service } from '../page';
|
||||
import { PrismaClient } from '@repo/db';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const generateToken = async (service: Service) => {
|
||||
const session = await getServerSession();
|
||||
if (!session) return null;
|
||||
|
||||
const accessToken = Array.from({ length: 10 }, () =>
|
||||
Math.floor(Math.random() * 10)
|
||||
).join('');
|
||||
|
||||
const code = await prisma.oAuthToken.create({
|
||||
data: {
|
||||
clientId: service.id,
|
||||
userId: session.user.id,
|
||||
accessToken: accessToken,
|
||||
},
|
||||
});
|
||||
return code;
|
||||
};
|
||||
26
apps/hub/app/(auth)/oauth/page.tsx
Normal file
26
apps/hub/app/(auth)/oauth/page.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Authorize } from './_components/Authorize';
|
||||
|
||||
export const services = [
|
||||
{
|
||||
id: '123456',
|
||||
service: 'dispatch',
|
||||
name: 'Leitstellendisposition',
|
||||
approvedUrls: ['http://localhost:3001'],
|
||||
},
|
||||
];
|
||||
export type Service = (typeof services)[number];
|
||||
|
||||
export default async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
}) => {
|
||||
const { service: serviceId } = await searchParams;
|
||||
const service = services.find((service) => service.id === serviceId);
|
||||
|
||||
if (!service) {
|
||||
return <div>Service not found</div>;
|
||||
}
|
||||
|
||||
return <Authorize service={service} />;
|
||||
};
|
||||
@@ -17,8 +17,8 @@ export const Register = () => {
|
||||
}),
|
||||
firstname: z.string().min(2).max(30),
|
||||
lastname: z.string().min(2).max(30),
|
||||
password: z.string().min(6),
|
||||
passwordConfirm: z.string().min(6),
|
||||
password: z.string(),
|
||||
passwordConfirm: z.string(),
|
||||
})
|
||||
.superRefine(({ password, passwordConfirm }, ctx) => {
|
||||
if (password !== passwordConfirm) {
|
||||
|
||||
@@ -3,22 +3,7 @@ import { Register } from './_components/Register';
|
||||
export default () => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="hero min-h-screen"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'url(https://img.daisyui.com/images/stock/photo-1507358522600-9f71e620c44e.webp)',
|
||||
}}
|
||||
>
|
||||
<div className="hero-overlay bg-opacity-60"></div>
|
||||
<div className="hero-content text-neutral-content text-center ">
|
||||
<div className="max-w-lg">
|
||||
<div className="card bg-base-100 w-full min-w-[500px] shadow-2xl max-md:min-w-[400px]">
|
||||
<Register />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Register />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { AuthOptions } from 'next-auth';
|
||||
import {
|
||||
AuthOptions,
|
||||
getServerSession as getNextAuthServerSession,
|
||||
} from 'next-auth';
|
||||
import { PrismaAdapter } from '@next-auth/prisma-adapter';
|
||||
import Credentials from 'next-auth/providers/credentials';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
@@ -29,40 +32,28 @@ export const options: AuthOptions = {
|
||||
},
|
||||
}),
|
||||
],
|
||||
secret: process.env.SECRET,
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
session: {
|
||||
strategy: 'jwt',
|
||||
maxAge: 30 * 24 * 60 * 60,
|
||||
},
|
||||
|
||||
adapter: PrismaAdapter(prisma),
|
||||
events: {
|
||||
async signIn(message) {
|
||||
console.log('Signed in!', { message });
|
||||
},
|
||||
async signOut(message) {
|
||||
console.log('Signed out!', { message });
|
||||
},
|
||||
async createUser(message) {
|
||||
console.log('User created!', { message });
|
||||
},
|
||||
},
|
||||
callbacks: {
|
||||
jwt: async ({ token, user }) => {
|
||||
if (user) {
|
||||
token.uid = user;
|
||||
if (user && 'firstname' in user) {
|
||||
return {
|
||||
...token,
|
||||
...user,
|
||||
};
|
||||
}
|
||||
|
||||
return token;
|
||||
},
|
||||
session: async ({ session, token }: any) => {
|
||||
// here we put session.useData and put inside it whatever you want to be in the session
|
||||
// here try to console.log(token) and see what it will have
|
||||
// sometimes the user get stored in token.uid.userData
|
||||
// sometimes the user data get stored in just token.uid
|
||||
session.userData = token.uid.userData;
|
||||
|
||||
return session;
|
||||
session: async ({ session, user, token }) => {
|
||||
return {
|
||||
...session,
|
||||
user: token,
|
||||
};
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
@@ -72,3 +63,5 @@ export const options: AuthOptions = {
|
||||
newUser: '/register',
|
||||
},
|
||||
} satisfies AuthOptions;
|
||||
|
||||
export const getServerSession = async () => getNextAuthServerSession(options);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from 'next/font/google';
|
||||
import './globals.css';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { NextAuthSessionProvider } from './_components/AuthSessionProvider';
|
||||
import { options } from './api/auth/[...nextauth]/auth';
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: '--font-geist-sans',
|
||||
@@ -24,8 +25,7 @@ export default async function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const session = await getServerSession();
|
||||
|
||||
const session = await getServerSession(options);
|
||||
return (
|
||||
<html lang="en">
|
||||
<NextAuthSessionProvider session={session}>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useSession } from 'next-auth/react';
|
||||
import Link from 'next/link';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function Home() {
|
||||
const { data: session, status, update } = useSession();
|
||||
console.log(session, status);
|
||||
const { data: session, update } = useSession();
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, []);
|
||||
@@ -14,6 +14,9 @@ export default function Home() {
|
||||
<h1 className="text-5xl">Hub</h1>
|
||||
{!session && <h2 className="text-error text-xl">Not signed in</h2>}
|
||||
{session?.user?.firstname && <h1>Hi, {session?.user?.firstname}</h1>}
|
||||
<Link href="/logout">
|
||||
<button className="btn">Logout</button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev": "next dev --turbopack -p 3000",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
|
||||
7
apps/hub/types/next-auth.d.ts
vendored
7
apps/hub/types/next-auth.d.ts
vendored
@@ -1,18 +1,19 @@
|
||||
import NextAuth from 'next-auth';
|
||||
import { User } from '@repo/db';
|
||||
import { User as IUser } from '@repo/db';
|
||||
|
||||
declare module 'next-auth' {
|
||||
/**
|
||||
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
||||
*/
|
||||
interface Session {
|
||||
user: User;
|
||||
user: IUser;
|
||||
}
|
||||
type User = User;
|
||||
type User = IUser;
|
||||
}
|
||||
|
||||
declare module 'next-auth/jwt' {
|
||||
interface JWT {
|
||||
uid: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
email: string;
|
||||
|
||||
Reference in New Issue
Block a user