feat: add homepage & auth

This commit is contained in:
2024-02-23 19:36:10 +07:00
parent d579c1b7c5
commit 735fa9354b
42 changed files with 981 additions and 70 deletions
+3 -3
View File
@@ -1,7 +1,7 @@
import { Request } from "express";
import { CreateExpressContextOptions } from "@trpc/server/adapters/express";
export const createContext = async ({ req }: { req: Request }) => {
return {};
export const createContext = async (ctx: CreateExpressContextOptions) => {
return ctx;
};
export type Context = Awaited<ReturnType<typeof createContext>>;
+1 -1
View File
@@ -5,7 +5,7 @@ import { PageContext } from "vike/types";
const trpcServer = async (ctx: PageContext) => {
const createCaller = createCallerFactory(appRouter);
const context = await createContext({ req: ctx.req });
const context = await createContext(ctx);
return createCaller(context);
};
+2 -1
View File
@@ -17,4 +17,5 @@ export const user = sqliteTable("users", {
export const insertUserSchema = createInsertSchema(user);
export const selectUserSchema = createSelectSchema(user);
export type UserSchema = z.infer<typeof selectUserSchema>;
export type UserWithPassword = z.infer<typeof selectUserSchema>;
export type UserSchema = Omit<UserWithPassword, "password">;
+4 -1
View File
@@ -1,8 +1,10 @@
import "dotenv/config";
import express from "express";
import { renderPage } from "vike/server";
import { IS_DEV } from "./lib/consts";
import cookieParser from "cookie-parser";
import api from "./api";
import { authMiddleware } from "./middlewares/auth";
async function createServer() {
const app = express();
@@ -24,12 +26,13 @@ async function createServer() {
}
app.use(cookieParser());
app.use(authMiddleware);
app.use("/api", api);
app.use("*", async (req, res, next) => {
const url = req.originalUrl;
const pageContext = { req, cookies: req.cookies };
const pageContext = { req, res, cookies: req.cookies, user: req.user };
const ctx = await renderPage({ urlOriginal: url, ...pageContext });
const { httpResponse } = ctx;
+26
View File
@@ -0,0 +1,26 @@
import jwt from "jsonwebtoken";
const JWT_KEY =
process.env.JWT_KEY || "e2b185bd471dda7a14cb8d9cfb8d1c568ac27926";
export type TokenData = {
id: number;
};
export const createToken = (payload: TokenData) => {
return new Promise<string>((resolve, reject) => {
jwt.sign(payload, JWT_KEY, { expiresIn: "30d" }, function (err, token) {
if (err) return reject(err);
resolve(token as string);
});
});
};
export const verifyToken = (token: string) => {
return new Promise<TokenData>((resolve, reject) => {
jwt.verify(token, JWT_KEY, function (err, decoded) {
if (err) return reject(err);
resolve(decoded as TokenData);
});
});
};
+38
View File
@@ -0,0 +1,38 @@
import { NextFunction, Request, Response } from "express";
import { verifyToken } from "../lib/jwt";
import db from "../db";
import { and, eq, isNull } from "drizzle-orm";
import { user } from "../db/schema/user";
export const authMiddleware = async (
req: Request,
_res: Response,
next: NextFunction
) => {
try {
const token = req.cookies["auth-token"];
if (!token) {
throw new Error("No token in cookies.");
}
const data = await verifyToken(token);
if (!data?.id) {
throw new Error("Invalid token!");
}
const userData = await db.query.user.findFirst({
where: and(eq(user.id, data.id), isNull(user.deletedAt)),
columns: { password: false },
});
if (!userData) {
throw new Error("User is not found!");
}
// store userdata to every request
(req as any).user = userData;
} catch (err) {
//
}
next();
};
+2
View File
@@ -1,8 +1,10 @@
import { router } from "../api/trpc";
import auth from "./auth";
import project from "./project";
import file from "./file";
export const appRouter = router({
auth,
project,
file,
});
+54
View File
@@ -0,0 +1,54 @@
import { z } from "zod";
import { procedure, router } from "../api/trpc";
import db from "../db";
import { TRPCError } from "@trpc/server";
import { and, eq, isNull } from "drizzle-orm";
import { user } from "../db/schema/user";
import { verifyPassword } from "../lib/crypto";
import { createToken } from "../lib/jwt";
const authRouter = router({
login: procedure
.input(
z.object({
email: z.string().email(),
password: z.string().min(1),
})
)
.mutation(async ({ ctx, input }) => {
const userData = await db.query.user.findFirst({
where: and(eq(user.email, input.email), isNull(user.deletedAt)),
});
if (!userData) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Email is not found!",
});
}
if (!(await verifyPassword(userData.password, input.password))) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid password!",
});
}
// set user token
const token = await createToken({ id: userData.id });
ctx.res.cookie("auth-token", token, { httpOnly: true });
return { ...userData, password: undefined };
}),
logout: procedure.mutation(({ ctx }) => {
ctx.res.cookie("auth-token", null, {
httpOnly: true,
expires: new Date(0),
});
return true;
}),
});
export default authRouter;
+73 -11
View File
@@ -9,6 +9,11 @@ import {
import { z } from "zod";
import { TRPCError } from "@trpc/server";
import { uid } from "../lib/utils";
import { insertUserSchema, user } from "../db/schema/user";
import { faker } from "@faker-js/faker";
import { ucwords } from "~/lib/utils";
import { hashPassword } from "../lib/crypto";
import { createToken } from "../lib/jwt";
const projectRouter = router({
getAll: procedure.query(async () => {
@@ -45,19 +50,59 @@ const projectRouter = router({
create: procedure
.input(
insertProjectSchema.pick({
title: true,
})
insertProjectSchema
.pick({
title: true,
})
.merge(
z.object({
forkFromId: z.number().optional(),
user: insertUserSchema
.pick({
name: true,
email: true,
password: true,
})
.optional(),
})
)
)
.mutation(async ({ input }) => {
const data: z.infer<typeof insertProjectSchema> = {
userId: 1,
title: input.title,
slug: uid(),
};
.mutation(async ({ ctx, input }) => {
const title =
input.title.length > 0 ? input.title : ucwords(faker.lorem.words(2));
let userId = 0;
const [result] = await db.insert(project).values(data).returning();
return result;
return db.transaction(async (tx) => {
if (input.user) {
const [usr] = await tx
.insert(user)
.values({
...input.user,
password: await hashPassword(input.user.password),
})
.returning();
userId = usr.id;
// set user token
const token = await createToken({ id: userId });
ctx.res.cookie("auth-token", token, { httpOnly: true });
}
if (!userId) {
throw new Error("Invalid userId!");
}
const data: z.infer<typeof insertProjectSchema> = {
userId,
title,
slug: uid(),
};
const [result] = await tx.insert(project).values(data).returning();
return result;
});
}),
update: procedure
@@ -95,6 +140,23 @@ const projectRouter = router({
return result;
}),
getTemplates: procedure.query(() => {
return [
{
title: "Empty Project",
projectId: 0,
},
{
title: "Vanilla HTML+CSS+JS",
projectId: 1,
},
{
title: "React + Tailwindcss",
projectId: 2,
},
];
}),
});
export default projectRouter;
+11
View File
@@ -0,0 +1,11 @@
import type { UserSchema } from "./db/schema/user";
declare global {
namespace Express {
interface Request {
user?: UserSchema | null;
}
}
}
export {};