Skip to content

Commit

Permalink
Merge pull request #240 from parazeeknova/development
Browse files Browse the repository at this point in the history
feat[SIGNUP]: Signup mods - batch merge
  • Loading branch information
parazeeknova authored Jan 13, 2025
2 parents d751896 + 1782a08 commit 0341e4d
Show file tree
Hide file tree
Showing 18 changed files with 1,271 additions and 238 deletions.
5 changes: 2 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,8 @@ STREAM_SECRET=""
# =================================================================
# EMAIL SERVICE
# =================================================================
# NodeMailer Configuration
GMAIL_USER="" # Your Gmail address
GMAIL_APP_PASSWORD="" # Gmail App Password (not regular password)
# Unsend (email)
UNSEND_API_KEY=""

# =================================================================
# MAINTENANCE & OPERATIONS
Expand Down
7 changes: 7 additions & 0 deletions apps/web/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
FROM node:latest AS base

ARG UNSEND_API_KEY

FROM base AS builder
RUN apt-get update && apt-get install -y git
WORKDIR /app
RUN npm install -g pnpm@latest turbo@latest

ENV UNSEND_API_KEY=$UNSEND_API_KEY

COPY . .
RUN turbo prune --scope=@zephyr/web --docker

Expand All @@ -13,6 +17,8 @@ RUN apt-get update && apt-get install -y

WORKDIR /app

ENV UNSEND_API_KEY=$UNSEND_API_KEY

COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=builder /app/turbo.json ./turbo.json
Expand Down Expand Up @@ -46,6 +52,7 @@ ENV PATH="/app/node_modules/.bin:${PATH}"
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV TURBO_TELEMETRY_DISABLED=1
ENV UNSEND_API_KEY=$UNSEND_API_KEY

USER nextjs

Expand Down
4 changes: 2 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@node-rs/argon2": "^2.0.2",
"@prisma/client": "^6.1.0",
"@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-icons": "^1.3.2",
Expand Down Expand Up @@ -66,7 +67,6 @@
"mime-types": "^2.1.35",
"next": "15.1.3",
"next-themes": "^0.4.4",
"nodemailer": "^6.9.16",
"prism-react-renderer": "^2.4.1",
"process": "^0.11.10",
"qrcode.react": "^4.2.0",
Expand All @@ -86,6 +86,7 @@
"stream-chat-react": "^12.8.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"unsend": "^1.3.0",
"usehooks-ts": "^3.1.0",
"util": "^0.12.5",
"webpack": "^5.97.1",
Expand All @@ -96,7 +97,6 @@
"@types/jsonwebtoken": "^9.0.7",
"@types/mime-types": "^2.1.4",
"@types/node": "^22.10.3",
"@types/nodemailer": "^6.4.17",
"@types/react": "19.0.2",
"@types/react-dom": "19.0.1",
"@types/react-syntax-highlighter": "^15.5.13",
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/app/(auth)/client/ClientSignUpPage.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
"use client";

import signupImage from "@zephyr-assets/signup-image.jpg";
import SignUpForm from "@zephyr-ui/Auth/SignUpForm";
import { AnimatePresence, motion } from "framer-motion";
import Image from "next/image";
import Link from "next/link";

import signupImage from "@zephyr-assets/signup-image.jpg";
import SignUpForm from "@zephyr-ui/Auth/SignUpForm";

const fadeIn = {
hidden: { opacity: 0 },
visible: {
Expand Down
188 changes: 152 additions & 36 deletions apps/web/src/app/(auth)/signup/actions.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,125 @@
"use server";

import { hash } from "@node-rs/argon2";
import { createStreamUser } from "@zephyr/auth/src";
import { sendVerificationEmail } from "@zephyr/auth/src/email/service";
import { createStreamUser, lucia } from "@zephyr/auth/src";
import {
isDevelopmentMode,
isEmailServiceConfigured,
sendVerificationEmail
} from "@zephyr/auth/src/email/service";
import { EMAIL_ERRORS, isEmailValid } from "@zephyr/auth/src/email/validation";
import { resendVerificationEmail } from "@zephyr/auth/src/verification/resend";
import { type SignUpValues, signUpSchema } from "@zephyr/auth/validation";
import { prisma } from "@zephyr/db";
import jwt from "jsonwebtoken";
import { generateIdFromEntropySize } from "lucia";
import { cookies } from "next/headers";

interface SignUpResponse {
verificationUrl?: string;
error?: string;
success: boolean;
skipVerification?: boolean;
message?: string;
}

const USERNAME_ERRORS = {
TAKEN: "This username is already taken. Please choose another.",
INVALID: "Username can only contain letters, numbers, and underscores.",
REQUIRED: "Username is required"
REQUIRED: "Username is required",
CREATION_FAILED: "Failed to create user account"
};

const SYSTEM_ERRORS = {
JWT_SECRET: "System configuration error: JWT_SECRET is not configured",
USER_ID: "Failed to generate user ID",
TOKEN: "Failed to generate verification token",
INVALID_PAYLOAD: "Invalid token payload data"
INVALID_PAYLOAD: "Invalid token payload data",
SESSION_CREATION: "Failed to create user session"
};

if (!process.env.JWT_SECRET) {
throw new Error(SYSTEM_ERRORS.JWT_SECRET);
async function createDevUser(
userId: string,
username: string,
email: string,
passwordHash: string
): Promise<void> {
try {
const newUser = await prisma.user.create({
data: {
id: userId,
username,
displayName: username,
email,
passwordHash,
emailVerified: true
}
});

await createStreamUser(newUser.id, newUser.username, newUser.displayName);

// @ts-expect-error
const session = await lucia.createSession(userId, {});
const cookieStore = await cookies();
const sessionCookie = lucia.createSessionCookie(session.id);
cookieStore.set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes
);
} catch (error) {
console.error("Development user creation error:", error);
try {
await prisma.user.delete({ where: { id: userId } });
} catch (cleanupError) {
console.error("Cleanup failed:", cleanupError);
}
throw error;
}
}

async function createProdUser(
userId: string,
username: string,
email: string,
passwordHash: string,
verificationToken: string
): Promise<void> {
await prisma.$transaction(async (tx) => {
const newUser = await tx.user.create({
data: {
id: userId,
username,
displayName: username,
email,
passwordHash,
emailVerified: false
}
});

await tx.emailVerificationToken.create({
data: {
token: verificationToken,
userId,
expiresAt: new Date(Date.now() + 3600000)
}
});

await createStreamUser(newUser.id, newUser.username, newUser.displayName);
});
}

export { resendVerificationEmail };

export async function signUp(
credentials: SignUpValues
): Promise<{ error?: string; success?: boolean }> {
): Promise<SignUpResponse> {
try {
if (!credentials) {
return { error: "No credentials provided", success: false };
}

const validationResult = signUpSchema.safeParse(credentials);

if (!validationResult.success) {
const firstError = validationResult.error.errors[0];
return {
Expand Down Expand Up @@ -84,40 +165,75 @@ export async function signUp(
throw new Error(SYSTEM_ERRORS.USER_ID);
}

const tokenPayload = { userId, email, timestamp: Date.now() };
if (!process.env.JWT_SECRET) {
throw new Error(SYSTEM_ERRORS.JWT_SECRET);
const skipEmailVerification =
isDevelopmentMode() && !isEmailServiceConfigured();

if (skipEmailVerification) {
try {
await createDevUser(userId, username, email, passwordHash);
return {
success: true,
skipVerification: true,
message: "Development mode: Email verification skipped"
};
} catch (error) {
console.error("Development signup error:", error);
return {
error:
error instanceof Error
? error.message
: "Failed to create account in development mode",
success: false
};
}
}
const token = jwt.sign(tokenPayload, process.env.JWT_SECRET, {
expiresIn: "1h"
});

await prisma.$transaction(async (tx) => {
const newUser = await tx.user.create({
data: {
id: userId,
username,
displayName: username,
email,
passwordHash,
emailVerified: false
}
});
if (!process.env.JWT_SECRET) {
return {
error: SYSTEM_ERRORS.JWT_SECRET,
success: false
};
}

await tx.emailVerificationToken.create({
data: {
token,
userId,
expiresAt: new Date(Date.now() + 3600000)
}
try {
const tokenPayload = { userId, email, timestamp: Date.now() };
const verificationToken = jwt.sign(tokenPayload, process.env.JWT_SECRET, {
expiresIn: "1h"
});

await createStreamUser(newUser.id, newUser.username, newUser.displayName);
});
await createProdUser(
userId,
username,
email,
passwordHash,
verificationToken
);

const emailResult = await sendVerificationEmail(email, verificationToken);
if (!emailResult.success) {
await prisma.user.delete({ where: { id: userId } });
return {
error: emailResult.error || "Failed to send verification email",
success: false
};
}

await sendVerificationEmail(email, token);

return { success: true };
return {
success: true,
skipVerification: false
};
} catch (error) {
console.error("Production signup error:", error);
try {
await prisma.user.delete({ where: { id: userId } });
} catch (cleanupError) {
console.error("Cleanup failed:", cleanupError);
}
return {
error: "Failed to create account",
success: false
};
}
} catch (error) {
console.error("Signup error:", error);
return {
Expand Down
44 changes: 32 additions & 12 deletions apps/web/src/app/api/support/route.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
import { rateLimitMiddleware } from "@/lib/rate-limiter";
import { NextResponse } from "next/server";
import nodemailer from "nodemailer";
import { Unsend } from "unsend";

const transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 465,
secure: true,
auth: {
user: process.env.GMAIL_USER,
pass: process.env.GMAIL_APP_PASSWORD
let unsend: Unsend;

const initializeUnsend = () => {
if (!unsend && process.env.UNSEND_API_KEY) {
unsend = new Unsend(
process.env.UNSEND_API_KEY,
"https://mails.zephyyrr.in"
);
} else if (!process.env.UNSEND_API_KEY) {
console.error("Missing UNSEND_API_KEY environment variable");
}
});
};

const SENDER = "zephyyrr.in";

export async function POST(request: Request) {
try {
initializeUnsend();
if (!unsend) {
return NextResponse.json(
{ error: "Email service not initialized" },
{ status: 500 }
);
}

const forwardedFor = request.headers.get("x-forwarded-for");
const identifier = (forwardedFor?.split(",")[0] || "unknown").trim();

Expand Down Expand Up @@ -53,9 +66,16 @@ export async function POST(request: Request) {
</ul>`
: "";

await transporter.sendMail({
from: `"Zephyr Support" <${process.env.GMAIL_USER}>`,
to: process.env.GMAIL_USER,
if (!process.env.SUPPORT_EMAIL) {
return NextResponse.json(
{ error: "Support email not configured" },
{ status: 500 }
);
}

await unsend.emails.send({
from: `Zephyr Support <no-reply@${SENDER}>`,
to: process.env.SUPPORT_EMAIL,
subject: `[Zephyr Support - ${type}] ${subject}`,
html: `
<h2>New Support Request</h2>
Expand Down
Loading

0 comments on commit 0341e4d

Please sign in to comment.