Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions prisma/migrations/20251123051138_add_user_role/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "public"."UserRole" AS ENUM ('USER', 'DEMO');

-- AlterTable
ALTER TABLE "public"."User" ADD COLUMN "role" "public"."UserRole" NOT NULL DEFAULT 'USER';
6 changes: 6 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,17 @@ enum TransactionType {
EXPENSE
}

enum UserRole {
USER
DEMO
}

model User {
id String @id @default(cuid())
name String
email String @unique
password String
role UserRole @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
budgets Budget[]
Expand Down
9 changes: 3 additions & 6 deletions src/components/ui/RadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,11 @@ function RadioGroupItem({
}: RadioGroupItemProps) {
return (
<RacRadio {...props} className="group w-full">
{({ isHovered, isPressed, isSelected, isDisabled, isFocusVisible }) => (
{({ isPressed, isSelected, isDisabled, isFocusVisible }) => (
<div
className={cn(
"bg-primary border-primary group-rac-selected:border-brand group-rac-selected:bg-active relative flex cursor-pointer items-start justify-between gap-2 rounded-lg border px-3.5 py-3 text-sm",

// Hover
isHovered && "bg-primary_hover",

// Pressed
isPressed && "bg-primary_hover",

Expand Down Expand Up @@ -94,10 +91,10 @@ function RadioGroupItem({
initial={{ scale: 0 }}
animate={isSelected ? "selected" : "unSelected"}
variants={{
selected: { scale: 0.7, opacity: 1 },
selected: { scale: 0.75, opacity: 1 },
unSelected: { scale: 0, opacity: 0 },
}}
transition={{ type: "spring", stiffness: 350, damping: 30 }}
transition={{ type: "spring", stiffness: 375, damping: 30 }}
/>
</span>
</div>
Expand Down
18 changes: 0 additions & 18 deletions src/data-access/auth.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/data-access/lookups.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "server-only"

import { cache } from "react"

import { prisma, Prisma } from "@/lib/prisma"
Expand Down
2 changes: 1 addition & 1 deletion src/features/account-settings/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export async function deleteAccount(
_formData: FormData
): Promise<string | undefined> {
const response = await account.deleteAccount()
if (!response.success) return "Error deleting account. Please try again."
if (!response.success) return response.message

signOut()
}
75 changes: 55 additions & 20 deletions src/features/account-settings/data-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import bcrypt from "bcryptjs"
import { redirect } from "next/navigation"
import { cache } from "react"

import { verifySession } from "@/data-access/auth"
import { DEMO_ACCOUNT_ERROR_MESSAGE } from "@/lib/constants"
import { prisma } from "@/lib/prisma"
import {
import { verifySession } from "@/lib/session"

import type {
DALReturn,
DALDeleteItemReurn,
EmailUpdateErrors,
NameUpdateErrors,
PasswordUpdateErrors,
Expand All @@ -18,11 +21,11 @@ import {
// ============================================

export const getUser = cache(async () => {
const userId = await verifySession()
if (!userId) redirect("/login")
const session = await verifySession()
if (!session) redirect("/login")

const user = await prisma.user.findUnique({
where: { id: userId },
where: { id: session.userId },
select: { id: true, email: true, name: true },
})

Expand All @@ -39,12 +42,19 @@ export async function updateName({
}: {
name: string
}): Promise<DALReturn<NameUpdateErrors>> {
const userId = await verifySession()
if (!userId) redirect("/login")
const session = await verifySession()
if (!session) redirect("/login")

if (session.role === "DEMO") {
return {
success: false,
fieldErrors: { name: [DEMO_ACCOUNT_ERROR_MESSAGE] },
}
}

try {
await prisma.user.update({
where: { id: userId },
where: { id: session.userId },
data: { name },
})
return { success: true }
Expand All @@ -66,12 +76,19 @@ export async function updateEmail({
}: {
email: string
}): Promise<DALReturn<EmailUpdateErrors>> {
const userId = await verifySession()
if (!userId) redirect("/login")
const session = await verifySession()
if (!session) redirect("/login")

if (session.role === "DEMO") {
return {
success: false,
fieldErrors: { email: [DEMO_ACCOUNT_ERROR_MESSAGE] },
}
}

try {
await prisma.user.update({
where: { id: userId },
where: { id: session.userId },
data: { email },
})
return { success: true }
Expand All @@ -95,12 +112,19 @@ export async function updatePassword({
currentPassword: string
newPassword: string
}): Promise<DALReturn<PasswordUpdateErrors>> {
const userId = await verifySession()
if (!userId) redirect("/login")
const session = await verifySession()
if (!session) redirect("/login")

if (session.role === "DEMO") {
return {
success: false,
fieldErrors: { currentPassword: [DEMO_ACCOUNT_ERROR_MESSAGE] },
}
}

try {
const user = await prisma.user.findUnique({
where: { id: userId },
where: { id: session.userId },
select: { password: true },
})

Expand All @@ -119,7 +143,7 @@ export async function updatePassword({
const newPasswordHashed = await bcrypt.hash(newPassword, 12)

await prisma.user.update({
where: { id: userId },
where: { id: session.userId },
data: { password: newPasswordHashed },
})
return { success: true }
Expand All @@ -136,15 +160,26 @@ export async function updatePassword({
// ============== Delete Account ==============
// ============================================

export async function deleteAccount(): Promise<{ success: boolean }> {
const userId = await verifySession()
if (!userId) redirect("/login")
export async function deleteAccount(): Promise<DALDeleteItemReurn> {
const session = await verifySession()
if (!session) redirect("/login")

if (session.role === "DEMO") {
return {
success: false,
message:
"You are not allowed to delete the demo account. Please create a free account to manage your own data.",
}
}

try {
await prisma.user.delete({ where: { id: userId } })
await prisma.user.delete({ where: { id: session.userId } })
return { success: true }
} catch (e) {
console.error(e)
return { success: false }
return {
success: false,
message: "Error deleting account. Please try again.",
}
}
}
12 changes: 3 additions & 9 deletions src/features/auth/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,12 @@ export async function registerUser(

const passwordHash = await bcrypt.hash(parsed.data.password, 12)

// return {
// email: [
// "New account creation is temporarily unavailable due to maintenance.",
// ],
// }

try {
const user = await prisma.user.create({
data: { ...parsed.data, password: passwordHash },
select: { id: true },
})
await createSession({ userId: user.id })
await createSession({ userId: user.id, role: "USER" })
} catch (e) {
console.error("Server Error:", e)
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002")
Expand All @@ -65,7 +59,7 @@ export async function loginUser(
try {
const user = await prisma.user.findUnique({
where: { email: parsed.data.email },
select: { id: true, password: true },
select: { id: true, password: true, role: true },
})
if (!user)
return {
Expand All @@ -79,7 +73,7 @@ export async function loginUser(
)
if (!isPasswordValid)
return { password: ["Incorrect password. Please try again."] }
await createSession({ userId: user.id })
await createSession({ userId: user.id, role: user.role })
} catch (e) {
console.error("Server Error:", e)
return { email: ["Something went wrong. Please try again."] }
Expand Down
30 changes: 30 additions & 0 deletions src/features/auth/components/DemoLogin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client"

import { useForm } from "react-hook-form"

import Button from "@/components/ui/Button"
import { loginUser } from "@/features/auth/actions"

export default function DemoLogin() {
const form = useForm({
defaultValues: { email: "johndoe@example.com", password: "John1234" },
})
return (
<form
className="w-full"
onSubmit={form.handleSubmit((data) => loginUser(data))}
>
<input type="hidden" {...form.register("email")} />
<input type="hidden" {...form.register("password")} />
<Button
className="w-full"
variant="secondary"
size="xl"
type="submit"
isPending={form.formState.isSubmitting}
>
Try Demo Account
</Button>
</form>
)
}
5 changes: 5 additions & 0 deletions src/features/auth/components/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import LogoBadge from "@/components/icons/LogoBadge"
import Button from "@/components/ui/Button"
import TextField from "@/components/ui/TextField"
import { loginUser } from "@/features/auth/actions"
import DemoLogin from "@/features/auth/components/DemoLogin"
import { loginSchema } from "@/features/auth/schemas"
import { setErrorsFromServer } from "@/lib/utils"

Expand Down Expand Up @@ -97,6 +98,10 @@ export default function LoginForm() {
Sign up
</Link>
</p>

<div className="border-secondary w-full border-t" />

<DemoLogin />
</div>
)
}
5 changes: 5 additions & 0 deletions src/features/auth/components/SignupForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import LogoBadge from "@/components/icons/LogoBadge"
import Button from "@/components/ui/Button"
import TextField from "@/components/ui/TextField"
import { registerUser } from "@/features/auth/actions"
import DemoLogin from "@/features/auth/components/DemoLogin"
import { signupSchema } from "@/features/auth/schemas"
import { setErrorsFromServer } from "@/lib/utils"

Expand Down Expand Up @@ -113,6 +114,10 @@ export default function SignupForm() {
Log in
</Link>
</p>

<div className="border-secondary w-full border-t" />

<DemoLogin />
</div>
)
}
2 changes: 1 addition & 1 deletion src/features/budgets/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export async function deleteBudget(
if (!parsed.success) return "Error deleting budget. Please try agian."

const response = await budgets.deleteBudget(parsed.data.id)
if (!response.success) return "Error deleting budget. Please try agian."
if (!response.success) return response.message ?? ""

revalidatePath("/budgets")
return null
Expand Down
Loading