diff --git a/auth.config.ts b/auth.config.ts index b728753..3fbe788 100644 --- a/auth.config.ts +++ b/auth.config.ts @@ -1,4 +1,5 @@ import type { NextAuthConfig } from 'next-auth'; +import Credentials from 'next-auth/providers/credentials'; export const authConfig = { pages: { @@ -7,15 +8,16 @@ export const authConfig = { callbacks: { authorized({ auth, request: { nextUrl } }) { const isLoggedIn = !!auth?.user; - const isOnDashboard = nextUrl.pathname.startsWith('/dashboard'); + console.log(`isLoggedIn: ${isLoggedIn}`) + const isOnDashboard = nextUrl.pathname.startsWith('/'); if (isOnDashboard) { if (isLoggedIn) return true; return false; // Redirect unauthenticated users to login page } else if (isLoggedIn) { - return Response.redirect(new URL('/dashboard', nextUrl)); + return Response.redirect(new URL('/', nextUrl)); } return true; }, }, - providers: [], // Add providers with an empty array for now + providers: [Credentials({})], // Add providers with an empty array for now } satisfies NextAuthConfig; diff --git a/auth.ts b/auth.ts index 10a9e6d..881bf9e 100644 --- a/auth.ts +++ b/auth.ts @@ -1,10 +1,15 @@ -import NextAuth, { User } from 'next-auth'; +import NextAuth from 'next-auth'; import { authConfig } from './auth.config'; import Credentials from 'next-auth/providers/credentials'; import { z } from 'zod'; import prisma from 'app/lib/db'; import bcrypt from 'bcrypt'; +export type User = { + id: number, + email: string, + password: string +} async function getUser(email: string): Promise { try { @@ -29,14 +34,20 @@ export const { auth, signIn, signOut } = NextAuth({ .object({ email: z.string().email(), password: z.string().min(6) }) .safeParse(credentials); + console.log(`parsed credentials: ${JSON.stringify(parsedCredentials)}`) if (parsedCredentials.success) { const { email, password } = parsedCredentials.data; const user = await getUser(email); + console.log(`USER:${user}`) if (!user) return null; + console.log(`checking string "ยท${password}" against hash "${user.password}"`) const passwordsMatch = await bcrypt.compare(password, user.password); - if (passwordsMatch) return user; + if (passwordsMatch) { + console.log("passwords match!") + return user + }; } console.log('Invalid credentials'); diff --git a/prisma/dev.db b/prisma/dev.db index 322c241..142b516 100644 Binary files a/prisma/dev.db and b/prisma/dev.db differ diff --git a/prisma/dev.db-journal b/prisma/dev.db-journal deleted file mode 100644 index 43bde4a..0000000 Binary files a/prisma/dev.db-journal and /dev/null differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fdec280..7b926bd 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,8 @@ import "./globals.css"; import Navlinks from "./ui/navLinks"; import { ModeToggle } from "./ui/modeToggle"; import { inter } from "./ui/fonts"; +import { signOut } from "../../auth"; +import { LogOut } from "lucide-react"; @@ -37,6 +39,19 @@ export default function RootLayout({

The self-hosted literary submission tracker.

+ +
{ + 'use server'; + await signOut(); + }} + > + +
+
diff --git a/src/app/lib/actions.ts b/src/app/lib/actions.ts new file mode 100644 index 0000000..d193ef9 --- /dev/null +++ b/src/app/lib/actions.ts @@ -0,0 +1,25 @@ +'use server'; + +import { signIn } from "../../../auth"; +import { AuthError } from 'next-auth'; + +// ... + +export async function authenticate( + prevState: string | undefined, + formData: FormData, +) { + try { + await signIn('credentials', formData); + } catch (error) { + if (error instanceof AuthError) { + switch (error.type) { + case 'CredentialsSignin': + return 'Invalid credentials.'; + default: + return 'Something went wrong.'; + } + } + throw error; + } +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 5c07299..8e37c7a 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,5 +1,4 @@ -import AcmeLogo from '@/app/ui/acme-logo'; -import LoginForm from '@/app/ui/login-form'; +import LoginForm from 'app/ui/login-form'; export default function LoginPage() { return ( @@ -7,7 +6,6 @@ export default function LoginPage() {
-
diff --git a/src/app/tailwind.css b/src/app/tailwind.css index 7d8b68c..1e22c18 100644 --- a/src/app/tailwind.css +++ b/src/app/tailwind.css @@ -634,6 +634,10 @@ body { border-width: 0; } +.pointer-events-none { + pointer-events: none; +} + .pointer-events-auto { pointer-events: auto; } @@ -666,6 +670,10 @@ body { left: 0.5rem; } +.left-3 { + left: 0.75rem; +} + .left-\[50\%\] { left: 50%; } @@ -686,6 +694,10 @@ body { top: 0px; } +.top-1\/2 { + top: 50%; +} + .top-2 { top: 0.5rem; } @@ -740,6 +752,10 @@ body { margin-bottom: 1.5rem; } +.mb-3 { + margin-bottom: 0.75rem; +} + .mb-4 { margin-bottom: 1rem; } @@ -764,6 +780,10 @@ body { margin-top: 1rem; } +.mt-5 { + margin-top: 1.25rem; +} + .mt-6 { margin-top: 1.5rem; } @@ -772,6 +792,10 @@ body { margin-top: auto; } +.block { + display: block; +} + .flex { display: flex; } @@ -792,6 +816,10 @@ body { display: grid; } +.hidden { + display: none; +} + .size-full { width: 100%; height: 100%; @@ -857,6 +885,14 @@ body { height: 1.2rem; } +.h-\[18px\] { + height: 18px; +} + +.h-\[48px\] { + height: 48px; +} + .h-\[var\(--radix-select-trigger-height\)\] { height: var(--radix-select-trigger-height); } @@ -922,10 +958,18 @@ body { width: 10rem; } +.w-5 { + width: 1.25rem; +} + .w-5\/6 { width: 83.333333%; } +.w-6 { + width: 1.5rem; +} + .w-7 { width: 1.75rem; } @@ -942,6 +986,10 @@ body { width: 1.2rem; } +.w-\[18px\] { + width: 18px; +} + .w-\[240px\] { width: 240px; } @@ -1000,10 +1048,18 @@ body { max-width: 20rem; } +.flex-1 { + flex: 1 1 0%; +} + .shrink-0 { flex-shrink: 0; } +.grow { + flex-grow: 1; +} + .caption-bottom { caption-side: bottom; } @@ -1012,6 +1068,11 @@ body { border-collapse: collapse; } +.-translate-y-1\/2 { + --tw-translate-y: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + .translate-x-\[-50\%\] { --tw-translate-x: -50%; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); @@ -1191,6 +1252,12 @@ body { margin-bottom: calc(0.625rem * var(--tw-space-y-reverse)); } +.space-y-3 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)); +} + .space-y-4 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); @@ -1269,6 +1336,11 @@ body { border-color: hsl(var(--destructive)); } +.border-gray-200 { + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); +} + .border-input { border-color: hsl(var(--input)); } @@ -1310,6 +1382,11 @@ body { background-color: hsl(var(--destructive)); } +.bg-gray-50 { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + .bg-input { background-color: hsl(var(--input)); } @@ -1395,6 +1472,11 @@ body { padding-right: 1rem; } +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + .px-8 { padding-left: 2rem; padding-right: 2rem; @@ -1430,6 +1512,19 @@ body { padding-bottom: 1rem; } +.py-\[9px\] { + padding-top: 9px; + padding-bottom: 9px; +} + +.pb-4 { + padding-bottom: 1rem; +} + +.pl-10 { + padding-left: 2.5rem; +} + .pl-3 { padding-left: 0.75rem; } @@ -1458,6 +1553,10 @@ body { padding-top: 0.25rem; } +.pt-8 { + padding-top: 2rem; +} + .text-left { text-align: left; } @@ -1470,6 +1569,11 @@ body { vertical-align: middle; } +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + .text-3xl { font-size: 1.875rem; line-height: 2.25rem; @@ -1569,6 +1673,21 @@ body { color: hsl(var(--foreground) / 0.5); } +.text-gray-50 { + --tw-text-opacity: 1; + color: rgb(249 250 251 / var(--tw-text-opacity)); +} + +.text-gray-500 { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} + +.text-gray-900 { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + .text-muted-foreground { color: hsl(var(--muted-foreground)); } @@ -1585,6 +1704,11 @@ body { color: hsl(var(--primary-foreground)); } +.text-red-500 { + --tw-text-opacity: 1; + color: rgb(239 68 68 / var(--tw-text-opacity)); +} + .text-secondary-foreground { color: hsl(var(--secondary-foreground)); } @@ -1648,6 +1772,10 @@ body { outline-style: solid; } +.outline-2 { + outline-width: 2px; +} + .ring-offset-background { --tw-ring-offset-color: hsl(var(--background)); } @@ -1925,6 +2053,16 @@ body { font-weight: 500; } +.placeholder\:text-gray-500::-moz-placeholder { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} + +.placeholder\:text-gray-500::placeholder { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} + .placeholder\:text-muted-foreground::-moz-placeholder { color: hsl(var(--muted-foreground)); } @@ -1973,10 +2111,20 @@ body { background-color: hsl(var(--secondary) / 0.8); } +.hover\:bg-sky-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(224 242 254 / var(--tw-bg-opacity)); +} + .hover\:text-accent-foreground:hover { color: hsl(var(--accent-foreground)); } +.hover\:text-blue-600:hover { + --tw-text-opacity: 1; + color: rgb(37 99 235 / var(--tw-text-opacity)); +} + .hover\:text-foreground:hover { color: hsl(var(--foreground)); } @@ -2106,6 +2254,11 @@ body { --tw-ring-offset-color: #dc2626; } +.peer:focus ~ .peer-focus\:text-gray-900 { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + .peer:disabled ~ .peer-disabled\:cursor-not-allowed { cursor: not-allowed; } @@ -2382,6 +2535,10 @@ body { margin-top: -8rem; } + .md\:block { + display: block; + } + .md\:h-36 { height: 9rem; } @@ -2397,6 +2554,23 @@ body { .md\:max-w-\[420px\] { max-width: 420px; } + + .md\:flex-none { + flex: none; + } + + .md\:justify-start { + justify-content: flex-start; + } + + .md\:p-2 { + padding: 0.5rem; + } + + .md\:px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; + } } .\[\&\:has\(\[aria-selected\]\)\]\:bg-accent:has([aria-selected]) { diff --git a/src/app/ui/login-form.tsx b/src/app/ui/login-form.tsx new file mode 100644 index 0000000..ff65168 --- /dev/null +++ b/src/app/ui/login-form.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { + AtSign, + KeyIcon, + CircleAlert, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +// import { useActionState } from 'react'; +import { useFormState } from 'react-dom'; +import { authenticate } from 'app/lib/actions'; +import { ArrowRightIcon } from 'lucide-react'; + +export default function LoginForm() { + const [errorMessage, formAction, isPending] = useFormState( + authenticate, + undefined, + ); + + return ( +
+
+

+ Please log in to continue. +

+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ {errorMessage && ( + <> + +

{errorMessage}

+ + )} +
+
+
+ ); +}