aboutsummaryrefslogtreecommitdiffstats
path: root/packages/web/server
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-02-22 15:32:40 +0000
committerMohamedBassem <me@mbassem.com>2024-02-22 15:32:40 +0000
commit942aac691225f4895c159a0260890ad2c576e0c9 (patch)
tree06a055fcd59c2753531f498ab58d0af4c7e8464c /packages/web/server
parent08e7cbcfcb5e0b992d10ada324712c224b7a4d07 (diff)
downloadkarakeep-942aac691225f4895c159a0260890ad2c576e0c9.tar.zst
feature: Add support for credentials registration and sign in
Diffstat (limited to 'packages/web/server')
-rw-r--r--packages/web/server/api/routers/_app.ts2
-rw-r--r--packages/web/server/api/routers/users.ts45
-rw-r--r--packages/web/server/api/trpc.ts2
-rw-r--r--packages/web/server/auth.ts81
4 files changed, 125 insertions, 5 deletions
diff --git a/packages/web/server/api/routers/_app.ts b/packages/web/server/api/routers/_app.ts
index 2097b47d..b958ef8f 100644
--- a/packages/web/server/api/routers/_app.ts
+++ b/packages/web/server/api/routers/_app.ts
@@ -1,9 +1,11 @@
import { router } from "../trpc";
import { apiKeysAppRouter } from "./apiKeys";
import { bookmarksAppRouter } from "./bookmarks";
+import { usersAppRouter } from "./users";
export const appRouter = router({
bookmarks: bookmarksAppRouter,
apiKeys: apiKeysAppRouter,
+ users: usersAppRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;
diff --git a/packages/web/server/api/routers/users.ts b/packages/web/server/api/routers/users.ts
new file mode 100644
index 00000000..aecec1d4
--- /dev/null
+++ b/packages/web/server/api/routers/users.ts
@@ -0,0 +1,45 @@
+import { zSignUpSchema } from "@/lib/types/api/users";
+import { publicProcedure, router } from "../trpc";
+import { Prisma, prisma } from "@hoarder/db";
+import { z } from "zod";
+import { hashPassword } from "@/server/auth";
+import { TRPCError } from "@trpc/server";
+
+export const usersAppRouter = router({
+ create: publicProcedure
+ .input(zSignUpSchema)
+ .output(
+ z.object({
+ name: z.string(),
+ email: z.string(),
+ }),
+ )
+ .mutation(async ({ input }) => {
+ try {
+ return await prisma.user.create({
+ data: {
+ name: input.name,
+ email: input.email,
+ password: await hashPassword(input.password),
+ },
+ select: {
+ name: true,
+ email: true,
+ },
+ });
+ } catch (e) {
+ if (e instanceof Prisma.PrismaClientKnownRequestError) {
+ if (e.code === "P2002") {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Email is already taken",
+ });
+ }
+ }
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: "Something went wrong",
+ });
+ }
+ }),
+});
diff --git a/packages/web/server/api/trpc.ts b/packages/web/server/api/trpc.ts
index e57d40d6..7df98372 100644
--- a/packages/web/server/api/trpc.ts
+++ b/packages/web/server/api/trpc.ts
@@ -31,7 +31,7 @@ export const publicProcedure = procedure;
export const authedProcedure = procedure.use(function isAuthed(opts) {
const user = opts.ctx.user;
- if (!user) {
+ if (!user || !user.id) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
diff --git a/packages/web/server/auth.ts b/packages/web/server/auth.ts
index b7391848..a63bcac4 100644
--- a/packages/web/server/auth.ts
+++ b/packages/web/server/auth.ts
@@ -5,8 +5,18 @@ import serverConfig from "@hoarder/shared/config";
import { prisma } from "@hoarder/db";
import { DefaultSession } from "next-auth";
import * as bcrypt from "bcrypt";
+import CredentialsProvider from "next-auth/providers/credentials";
import { randomBytes } from "crypto";
+import { Provider } from "next-auth/providers/index";
+
+declare module "next-auth/jwt" {
+ export interface JWT {
+ user: {
+ id: string;
+ } & DefaultSession["user"];
+ }
+}
declare module "next-auth" {
/**
@@ -19,19 +29,55 @@ declare module "next-auth" {
}
}
-const providers = [];
+const providers: Provider[] = [
+ CredentialsProvider({
+ // The name to display on the sign in form (e.g. "Sign in with...")
+ name: "Credentials",
+ credentials: {
+ email: { label: "Email", type: "email", placeholder: "Email" },
+ password: { label: "Password", type: "password" },
+ },
+ async authorize(credentials) {
+ if (!credentials) {
+ return null;
+ }
+
+ try {
+ return await validatePassword(
+ credentials?.email,
+ credentials?.password,
+ );
+ } catch (e) {
+ return null;
+ }
+ },
+ }),
+];
if (serverConfig.auth.authentik) {
providers.push(AuthentikProvider(serverConfig.auth.authentik));
}
export const authOptions: NextAuthOptions = {
- // Configure one or more authentication providers
adapter: PrismaAdapter(prisma),
providers: providers,
+ session: {
+ strategy: "jwt",
+ },
callbacks: {
- session({ session, user }) {
- session.user = { ...user };
+ async jwt({ token, user }) {
+ if (user) {
+ token.user = {
+ id: user.id,
+ name: user.name,
+ email: user.email,
+ image: user.image,
+ };
+ }
+ return token;
+ },
+ async session({ session, token }) {
+ session.user = { ...token.user };
return session;
},
},
@@ -110,3 +156,30 @@ export async function authenticateApiKey(key: string) {
return apiKey.user;
}
+
+export async function hashPassword(password: string) {
+ return bcrypt.hash(password, BCRYPT_SALT_ROUNDS);
+}
+
+export async function validatePassword(email: string, password: string) {
+ const user = await prisma.user.findUnique({
+ where: {
+ email,
+ },
+ });
+
+ if (!user) {
+ throw new Error("User not found");
+ }
+
+ if (!user.password) {
+ throw new Error("This user doesn't have a password defined");
+ }
+
+ const validation = await bcrypt.compare(password, user.password);
+ if (!validation) {
+ throw new Error("Wrong password");
+ }
+
+ return user;
+}