feat: add credentials authentication (#1)
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
|
||||
AUTH_SECRET=""
|
||||
AUTH_DISCORD_ID=""
|
||||
AUTH_DISCORD_SECRET=""
|
||||
AUTH_REDIRECT_PROXY_URL=""
|
||||
|
||||
NITRO_PRESET="vercel_edge"
|
||||
@@ -1,16 +0,0 @@
|
||||
# Auth Proxy
|
||||
|
||||
This is a simple proxy server that enables OAuth authentication for preview environments.
|
||||
|
||||
## Setup
|
||||
|
||||
Deploy it somewhere (Vercel is a one-click, zero-config option) and set the following environment variables:
|
||||
|
||||
- `AUTH_DISCORD_ID` - The Discord OAuth client ID
|
||||
- `AUTH_DISCORD_SECRET` - The Discord OAuth client secret
|
||||
- `AUTH_REDIRECT_PROXY_URL` - The URL of this proxy server
|
||||
- `AUTH_SECRET` - Your secret
|
||||
|
||||
Make sure the `AUTH_SECRET` and `AUTH_REDIRECT_PROXY_URL` match the values set for the main application's deployment for preview environments, and that you're using the same OAuth credentials for the proxy and the application's preview environment. The lines below shows what values should match eachother in both deployments.
|
||||
|
||||

|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "@alparr/auth-proxy",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nitro build",
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"dev": "nitro dev --port 3001",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.18.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@alparr/eslint-config": "workspace:^0.2.0",
|
||||
"@alparr/prettier-config": "workspace:^0.1.0",
|
||||
"@alparr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^8.53.0",
|
||||
"nitropack": "^2.8.1",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@alparr/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@alparr/prettier-config"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Auth } from "@auth/core";
|
||||
import Discord from "@auth/core/providers/discord";
|
||||
import { eventHandler, toWebRequest } from "h3";
|
||||
|
||||
export default eventHandler(async (event) =>
|
||||
Auth(toWebRequest(event), {
|
||||
secret: process.env.AUTH_SECRET,
|
||||
trustHost: !!process.env.VERCEL,
|
||||
redirectProxyUrl: process.env.AUTH_REDIRECT_PROXY_URL,
|
||||
providers: [
|
||||
Discord({
|
||||
clientId: process.env.AUTH_DISCORD_ID,
|
||||
clientSecret: process.env.AUTH_DISCORD_SECRET,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "@alparr/tsconfig/base.json",
|
||||
"include": ["routes"]
|
||||
}
|
||||
@@ -6,12 +6,18 @@ import "@alparr/auth/env.mjs";
|
||||
const config = {
|
||||
reactStrictMode: true,
|
||||
/** Enables hot reloading for local packages without a build step */
|
||||
transpilePackages: ["@alparr/api", "@alparr/auth", "@alparr/db", "@alparr/ui"],
|
||||
transpilePackages: [
|
||||
"@alparr/api",
|
||||
"@alparr/auth",
|
||||
"@alparr/db",
|
||||
"@alparr/ui",
|
||||
"@alparr/validation",
|
||||
],
|
||||
/** We already do linting and typechecking as separate tasks in CI */
|
||||
eslint: { ignoreDuringBuilds: true },
|
||||
typescript: { ignoreBuildErrors: true },
|
||||
experimental: {
|
||||
optimizePackageImports: ['@mantine/core', '@mantine/hooks'],
|
||||
optimizePackageImports: ["@mantine/core", "@mantine/hooks"],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"@alparr/api": "workspace:^0.1.0",
|
||||
"@alparr/auth": "workspace:^0.1.0",
|
||||
"@alparr/db": "workspace:^0.1.0",
|
||||
"@alparr/ui": "workspace:^",
|
||||
"@alparr/ui": "workspace:^0.1.0",
|
||||
"@alparr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.3.1",
|
||||
"@mantine/dates": "^7.3.1",
|
||||
"@mantine/form": "^7.3.1",
|
||||
@@ -65,4 +66,4 @@
|
||||
]
|
||||
},
|
||||
"prettier": "@alparr/prettier-config"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
"postcss-preset-mantine": {},
|
||||
"postcss-simple-vars": {
|
||||
variables: {
|
||||
'mantine-breakpoint-xs': '36em',
|
||||
'mantine-breakpoint-sm': '48em',
|
||||
'mantine-breakpoint-md': '62em',
|
||||
'mantine-breakpoint-lg': '75em',
|
||||
'mantine-breakpoint-xl': '88em',
|
||||
"mantine-breakpoint-xs": "36em",
|
||||
"mantine-breakpoint-sm": "48em",
|
||||
"mantine-breakpoint-md": "62em",
|
||||
"mantine-breakpoint-lg": "75em",
|
||||
"mantine-breakpoint-xl": "88em",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
BIN
apps/nextjs/public/logo/alparr.png
Normal file
BIN
apps/nextjs/public/logo/alparr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
@@ -1,3 +1,14 @@
|
||||
export { GET, POST } from "@alparr/auth";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
export const runtime = "edge";
|
||||
import { createHandlers } from "@alparr/auth";
|
||||
|
||||
export const GET = async (req: NextRequest) => {
|
||||
return await createHandlers(isCredentialsRequest(req)).handlers.GET(req);
|
||||
};
|
||||
export const POST = async (req: NextRequest) => {
|
||||
return await createHandlers(isCredentialsRequest(req)).handlers.POST(req);
|
||||
};
|
||||
|
||||
const isCredentialsRequest = (req: NextRequest) => {
|
||||
return req.url.includes("credentials") && req.method === "POST";
|
||||
};
|
||||
|
||||
@@ -3,8 +3,6 @@ import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||
import { appRouter, createTRPCContext } from "@alparr/api";
|
||||
import { auth } from "@alparr/auth";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
/**
|
||||
* Configure basic CORS headers
|
||||
* You should extend this to match your needs
|
||||
|
||||
74
apps/nextjs/src/app/auth/login/_components/login-form.tsx
Normal file
74
apps/nextjs/src/app/auth/login/_components/login-form.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
PasswordInput,
|
||||
rem,
|
||||
Stack,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { signIn } from "@alparr/auth/client";
|
||||
import { signInSchema } from "@alparr/validation";
|
||||
|
||||
export const LoginForm = () => {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const form = useForm<FormType>({
|
||||
validate: zodResolver(signInSchema),
|
||||
initialValues: {
|
||||
name: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (values: FormType) => {
|
||||
setIsLoading(true);
|
||||
setError(undefined);
|
||||
await signIn("credentials", {
|
||||
...values,
|
||||
redirect: false,
|
||||
callbackUrl: "/",
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response?.ok) {
|
||||
throw response?.error;
|
||||
}
|
||||
|
||||
void router.push("/");
|
||||
})
|
||||
.catch((error: Error | string) => {
|
||||
setIsLoading(false);
|
||||
setError(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
<form onSubmit={form.onSubmit((v) => void handleSubmit(v))}>
|
||||
<Stack gap="lg">
|
||||
<TextInput label="Username" {...form.getInputProps("name")} />
|
||||
<PasswordInput label="Password" {...form.getInputProps("password")} />
|
||||
<Button type="submit" fullWidth loading={isLoading}>
|
||||
Login
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
{error && (
|
||||
<Alert icon={<IconAlertTriangle size={rem(16)} />} color="red">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
type FormType = z.infer<typeof signInSchema>;
|
||||
25
apps/nextjs/src/app/auth/login/page.tsx
Normal file
25
apps/nextjs/src/app/auth/login/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Card, Center, Stack, Text, Title } from "@mantine/core";
|
||||
|
||||
import { LogoWithTitle } from "~/components/layout/logo";
|
||||
import { LoginForm } from "./_components/login-form";
|
||||
|
||||
export default function Login() {
|
||||
return (
|
||||
<Center>
|
||||
<Stack align="center" mt="xl">
|
||||
<LogoWithTitle />
|
||||
<Stack gap={6} align="center">
|
||||
<Title order={3} fw={400} ta="center">
|
||||
Log in to your account
|
||||
</Title>
|
||||
<Text size="sm" c="gray.5" ta="center">
|
||||
Welcome back! Please enter your credentials
|
||||
</Text>
|
||||
</Stack>
|
||||
<Card bg="dark.8" w={64 * 6} maw="90vw">
|
||||
<LoginForm />
|
||||
</Card>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
70
apps/nextjs/src/app/init/user/_components/init-user-form.tsx
Normal file
70
apps/nextjs/src/app/init/user/_components/init-user-form.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { initUserSchema } from "@alparr/validation";
|
||||
|
||||
import { showErrorNotification, showSuccessNotification } from "~/notification";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export const InitUserForm = () => {
|
||||
const router = useRouter();
|
||||
const { mutateAsync, error, isPending } = api.user.initUser.useMutation();
|
||||
const form = useForm<FormType>({
|
||||
validate: zodResolver(initUserSchema),
|
||||
validateInputOnBlur: true,
|
||||
validateInputOnChange: true,
|
||||
initialValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
repeatPassword: "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (values: FormType) => {
|
||||
console.log(values);
|
||||
await mutateAsync(values, {
|
||||
onSuccess: () => {
|
||||
showSuccessNotification({
|
||||
title: "User created",
|
||||
message: "You can now log in",
|
||||
});
|
||||
router.push("/auth/login");
|
||||
},
|
||||
onError: () => {
|
||||
showErrorNotification({
|
||||
title: "User creation failed",
|
||||
message: error?.message ?? "Unknown error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
<form
|
||||
onSubmit={form.onSubmit(
|
||||
(v) => void handleSubmit(v),
|
||||
(err) => console.log(err),
|
||||
)}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<TextInput label="Username" {...form.getInputProps("username")} />
|
||||
<PasswordInput label="Password" {...form.getInputProps("password")} />
|
||||
<PasswordInput
|
||||
label="Repeat password"
|
||||
{...form.getInputProps("repeatPassword")}
|
||||
/>
|
||||
<Button type="submit" fullWidth loading={isPending}>
|
||||
Create user
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
type FormType = z.infer<typeof initUserSchema>;
|
||||
38
apps/nextjs/src/app/init/user/page.tsx
Normal file
38
apps/nextjs/src/app/init/user/page.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Card, Center, Stack, Text, Title } from "@mantine/core";
|
||||
|
||||
import { db } from "@alparr/db";
|
||||
|
||||
import { LogoWithTitle } from "~/components/layout/logo";
|
||||
import { InitUserForm } from "./_components/init-user-form";
|
||||
|
||||
export default async function InitUser() {
|
||||
const firstUser = await db.query.users.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (firstUser) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<Stack align="center" mt="xl">
|
||||
<LogoWithTitle />
|
||||
<Stack gap={6} align="center">
|
||||
<Title order={3} fw={400} ta="center">
|
||||
New Alparr installation
|
||||
</Title>
|
||||
<Text size="sm" c="gray.5" ta="center">
|
||||
Please create the initial administator user.
|
||||
</Text>
|
||||
</Stack>
|
||||
<Card bg="dark.8" w={64 * 6} maw="90vw">
|
||||
<InitUserForm />
|
||||
</Card>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import "@mantine/core/styles.css";
|
||||
import "@mantine/dates/styles.css";
|
||||
import "@mantine/notifications/styles.css";
|
||||
|
||||
import { MantineProvider, ColorSchemeScript } from '@mantine/core';
|
||||
import { headers } from "next/headers";
|
||||
import { ColorSchemeScript, MantineProvider } from "@mantine/core";
|
||||
import { Notifications } from "@mantine/notifications";
|
||||
|
||||
import { uiConfiguration } from "@alparr/ui";
|
||||
|
||||
import { TRPCReactProvider } from "./providers";
|
||||
@@ -29,14 +31,22 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export default function Layout(props: { children: React.ReactNode }) {
|
||||
const colorScheme = "dark";
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<ColorSchemeScript />
|
||||
<ColorSchemeScript defaultColorScheme={colorScheme} />
|
||||
</head>
|
||||
<body className={["font-sans", fontSans.variable].join(" ")}>
|
||||
<TRPCReactProvider headers={headers()}>
|
||||
<MantineProvider defaultColorScheme="dark" {...uiConfiguration}>{props.children}</MantineProvider>
|
||||
<MantineProvider
|
||||
defaultColorScheme={colorScheme}
|
||||
{...uiConfiguration}
|
||||
>
|
||||
<Notifications />
|
||||
{props.children}
|
||||
</MantineProvider>
|
||||
</TRPCReactProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { db } from "@alparr/db";
|
||||
import { Button, Stack, Title } from "@mantine/core";
|
||||
|
||||
import { auth } from "@alparr/auth";
|
||||
import { db } from "@alparr/db";
|
||||
|
||||
export default async function HomePage() {
|
||||
const currentSession = await auth();
|
||||
const users = await db.query.users.findMany();
|
||||
|
||||
return (
|
||||
@@ -9,6 +12,11 @@ export default async function HomePage() {
|
||||
<Title>Home</Title>
|
||||
<Button>Test</Button>
|
||||
<pre>{JSON.stringify(users)}</pre>
|
||||
{currentSession && (
|
||||
<span>
|
||||
Currently logged in as <b>{currentSession.user.name}</b>
|
||||
</span>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
17
apps/nextjs/src/components/layout/logo.tsx
Normal file
17
apps/nextjs/src/components/layout/logo.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import Image from "next/image";
|
||||
import { Group, Title } from "@mantine/core";
|
||||
|
||||
interface LogoProps {
|
||||
size: number;
|
||||
}
|
||||
|
||||
export const Logo = ({ size = 60 }: LogoProps) => (
|
||||
<Image src="/logo/alparr.png" alt="Alparr logo" width={size} height={size} />
|
||||
);
|
||||
|
||||
export const LogoWithTitle = () => (
|
||||
<Group gap={0}>
|
||||
<Logo size={48} />
|
||||
<Title order={1}>lparr</Title>
|
||||
</Group>
|
||||
);
|
||||
20
apps/nextjs/src/notification.tsx
Normal file
20
apps/nextjs/src/notification.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { rem } from "@mantine/core";
|
||||
import type { NotificationData } from "@mantine/notifications";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||
|
||||
type CommonNotificationProps = Pick<NotificationData, "title" | "message">;
|
||||
|
||||
export const showSuccessNotification = (props: CommonNotificationProps) =>
|
||||
notifications.show({
|
||||
...props,
|
||||
color: "teal",
|
||||
icon: <IconCheck size={rem(20)} />,
|
||||
});
|
||||
|
||||
export const showErrorNotification = (props: CommonNotificationProps) =>
|
||||
notifications.show({
|
||||
...props,
|
||||
color: "red",
|
||||
icon: <IconX size={rem(20)} />,
|
||||
});
|
||||
Reference in New Issue
Block a user