diff --git a/package.json b/package.json index 82568ef..c35ba99 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "wiki-antifa", + "name": "logipedia", "version": "0.1.0", "private": true, "type": "module", @@ -46,6 +46,7 @@ "@trpc/client": "^11.0.0-rc.446", "@trpc/react-query": "^11.0.0-rc.446", "@trpc/server": "^11.0.0-rc.446", + "argon2": "^0.41.1", "cheerio": "^1.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6077e4..20853c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ importers: '@trpc/server': specifier: ^11.0.0-rc.446 version: 11.0.0-rc.824(typescript@5.8.2) + argon2: + specifier: ^0.41.1 + version: 0.41.1 cheerio: specifier: ^1.0.0 version: 1.0.0 @@ -982,6 +985,10 @@ packages: '@paralleldrive/cuid2@2.2.2': resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} + '@phc/format@1.0.0': + resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} + engines: {node: '>=10'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2051,6 +2058,10 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argon2@0.41.1: + resolution: {integrity: sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==} + engines: {node: '>=16.17.0'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -3356,6 +3367,14 @@ packages: sass: optional: true + node-addon-api@8.3.1: + resolution: {integrity: sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==} + engines: {node: ^18 || ^20 || >= 21} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -4886,6 +4905,8 @@ snapshots: dependencies: '@noble/hashes': 1.7.1 + '@phc/format@1.0.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -5963,6 +5984,12 @@ snapshots: arg@5.0.2: {} + argon2@0.41.1: + dependencies: + '@phc/format': 1.0.0 + node-addon-api: 8.3.1 + node-gyp-build: 4.8.4 + argparse@2.0.1: {} aria-hidden@1.2.4: @@ -7598,6 +7625,10 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-addon-api@8.3.1: {} + + node-gyp-build@4.8.4: {} + normalize-path@3.0.0: {} novel@1.0.2(@tiptap/extension-code-block@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5))(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): diff --git a/public/placeholder.svg b/public/placeholder.svg new file mode 100644 index 0000000..e763910 --- /dev/null +++ b/public/placeholder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/(PAGES)/me/_components/user-form.tsx b/src/app/(PAGES)/me/_components/user-form.tsx index 9d7327c..635d9ae 100644 --- a/src/app/(PAGES)/me/_components/user-form.tsx +++ b/src/app/(PAGES)/me/_components/user-form.tsx @@ -14,21 +14,21 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { z } from "zod"; -import { userProfileSchema } from "@/lib/validation/zod/user"; +import { userSchema } from "@/lib/validation/zod/user"; import { User } from "next-auth"; import { updateUserProfile } from "@/server/actions/user"; import { toast } from "sonner"; function UserForm({ server_user, cb }: { server_user: User; cb?: () => void }) { - const form = useForm>({ - resolver: zodResolver(userProfileSchema), + const form = useForm>({ + resolver: zodResolver(userSchema), defaultValues: { name: server_user?.name ?? "", }, }); // 2. Define a submit handler. - async function onSubmit(values: z.infer) { + async function onSubmit(values: z.infer) { // Do something with the form values. // ✅ This will be type-safe and validated. const { success } = await updateUserProfile(values); diff --git a/src/config/app.routes.ts b/src/config/app.routes.ts index 478ed40..3147482 100644 --- a/src/config/app.routes.ts +++ b/src/config/app.routes.ts @@ -22,6 +22,7 @@ export type AppRoutes = { // Auth routes signin: string; + signup: string; signout: string; profile: string; }; @@ -43,6 +44,7 @@ export const appRoutes: AppRoutes = { // auth signin: "/api/auth/signin", + signup: "/api/auth/signin", signout: "/api/auth/signout", profile: "/me", }; diff --git a/src/env.js b/src/env.js index 6b19f72..5f611cc 100644 --- a/src/env.js +++ b/src/env.js @@ -13,6 +13,8 @@ export const env = createEnv({ : z.string().optional(), AUTH_DISCORD_ID: z.string(), AUTH_DISCORD_SECRET: z.string(), + AUTH_GOOGLE_ID: z.string(), + AUTH_GOOGLE_SECRET: z.string(), DATABASE_URL: z.string().url(), NODE_ENV: z .enum(["development", "test", "production"]) @@ -36,6 +38,8 @@ export const env = createEnv({ AUTH_SECRET: process.env.AUTH_SECRET, AUTH_DISCORD_ID: process.env.AUTH_DISCORD_ID, AUTH_DISCORD_SECRET: process.env.AUTH_DISCORD_SECRET, + AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID, + AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET, DATABASE_URL: process.env.DATABASE_URL, NODE_ENV: process.env.NODE_ENV, }, diff --git a/src/lib/validation/zod/user.ts b/src/lib/validation/zod/user.ts index c0f982a..13c338b 100644 --- a/src/lib/validation/zod/user.ts +++ b/src/lib/validation/zod/user.ts @@ -1,7 +1,28 @@ import { z } from "zod"; -export const userProfileSchema = z.object({ +export const userSchema = z.object({ name: z.string().min(1), - // image: z.string().optional(), - // email: z.string().email(), + email: z.string().email(), + image: z.string().optional(), }); + +export const passwordSchema = z.string().min(8, { + message: "Passwort muss mindestens 8 Zeichen lang sein", +}); + +export const loginSchema = z.object({ + email: z.string().email(), + password: z.string().min(1), +}); + +export const registerSchema = z + .object({ + name: z.string().min(1), + email: z.string().email(), + password: passwordSchema, + confirmPassword: z.string(), + }) + .refine((data) => data.password === data.confirmPassword, { + message: "Passwörter stimmen nicht überein", + path: ["confirmPassword"], + }); diff --git a/src/server/actions/auth.ts b/src/server/actions/auth.ts new file mode 100644 index 0000000..04f6b14 --- /dev/null +++ b/src/server/actions/auth.ts @@ -0,0 +1,7 @@ +"use server"; + +import { signIn } from "@/server/auth"; + +export async function loginOAuth(provider: string) { + return await signIn(provider); +} diff --git a/src/server/actions/user.ts b/src/server/actions/user.ts index bdc9415..9efa055 100644 --- a/src/server/actions/user.ts +++ b/src/server/actions/user.ts @@ -1,13 +1,11 @@ "use server"; -import { userProfileSchema } from "@/lib/validation/zod/user"; +import { userSchema } from "@/lib/validation/zod/user"; import { api } from "@/trpc/server"; import { revalidatePath } from "next/cache"; import { z } from "zod"; -export async function updateUserProfile( - profile: z.infer, -) { +export async function updateUserProfile(profile: z.infer) { const [result] = await api.users.updateProfile({ profile }); if (!result?.id) return { success: false }; revalidatePath("/me"); diff --git a/src/server/api/root.ts b/src/server/api/root.ts index d7a44b5..6d8d0f8 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -4,6 +4,7 @@ import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc"; import { usersRouter } from "./routers/users"; import { authorRouter } from "./routers/author"; import { appRouter as globalRouter } from "./routers/app"; +import { authRouter } from "./routers/auth"; /** * This is the primary router for your server. * @@ -15,6 +16,7 @@ export const appRouter = createTRPCRouter({ users: usersRouter, author: authorRouter, app: globalRouter, + auth: authRouter, }); // export type definition of API diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth.ts new file mode 100644 index 0000000..2bcb59f --- /dev/null +++ b/src/server/api/routers/auth.ts @@ -0,0 +1,54 @@ +import { passwordSchema, userSchema } from "@/lib/validation/zod/user"; +import { createTRPCRouter, publicProcedure } from "../trpc"; +import { z } from "zod"; +import { eq } from "drizzle-orm"; +import { users } from "@/server/db/schema"; +import argon from "argon2"; + +export const authRouter = createTRPCRouter({ + register: publicProcedure + .input( + z.object({ + user: userSchema, + password: passwordSchema, + }), + ) + .mutation(async ({ ctx, input }) => { + const { + password, + user: { email, name }, + } = input; + // Check if user already exists + try { + const existingUser = await ctx.db.query.users.findFirst({ + where: eq(users.email, email), + }); + + if (existingUser) { + return { success: false, message: "User already exists" }; + } + + // Hash the password (12 is a good cost factor) + const hashedPassword = await argon.hash(password); + + // Create user in database + const [user] = await ctx.db + .insert(users) + .values({ + name, + email, + password: hashedPassword, + }) + .returning({ id: users.id }); + console.log(user); + + if (user) { + return { success: true, message: "User created successfully" }; + } + return { success: false, message: "Error creating user" }; + } catch (e) { + console.error(e); + return { success: false, message: "Error creating user" }; + } + }), +}); diff --git a/src/server/api/routers/users.ts b/src/server/api/routers/users.ts index cc7e6ce..e392642 100644 --- a/src/server/api/routers/users.ts +++ b/src/server/api/routers/users.ts @@ -3,11 +3,11 @@ import { createTRPCRouter, protectedProcedure } from "../trpc"; import { z } from "zod"; import { users } from "@/server/db/schema"; import { desc, eq } from "drizzle-orm"; -import { userProfileSchema } from "@/lib/validation/zod/user"; +import { userSchema } from "@/lib/validation/zod/user"; export const usersRouter = createTRPCRouter({ updateProfile: protectedProcedure - .input(z.object({ profile: userProfileSchema })) + .input(z.object({ profile: userSchema })) .mutation(async ({ ctx, input }) => { return await ctx.db .update(users) diff --git a/src/server/auth/config.ts b/src/server/auth/config.ts index 05020c1..360d024 100644 --- a/src/server/auth/config.ts +++ b/src/server/auth/config.ts @@ -1,6 +1,7 @@ import { DrizzleAdapter } from "@auth/drizzle-adapter"; import { type DefaultSession, type NextAuthConfig } from "next-auth"; import DiscordProvider from "next-auth/providers/discord"; +import GoogleProvider from "next-auth/providers/google"; import { db } from "@/server/db"; import { @@ -37,9 +38,18 @@ declare module "next-auth" { * * @see https://next-auth.js.org/configuration/options */ + +export const adapter = DrizzleAdapter(db, { + usersTable: users, + accountsTable: accounts, + sessionsTable: sessions, + verificationTokensTable: verificationTokens, +}) as Adapter; + export const authConfig = { providers: [ DiscordProvider, + GoogleProvider, /** * ...add more providers here. * @@ -50,19 +60,23 @@ export const authConfig = { * @see https://next-auth.js.org/providers/github */ ], - adapter: DrizzleAdapter(db, { - usersTable: users, - accountsTable: accounts, - sessionsTable: sessions, - verificationTokensTable: verificationTokens, - }) as Adapter, + // pages: { + // signIn: appRoutes.signin, // Custom sign in page + // }, + adapter, callbacks: { - session: ({ session, user }) => ({ - ...session, - user: { - ...session.user, - id: user.id, - }, - }), + session: ({ session, user }) => { + return { + ...session, + user: { + ...session.user, + id: user.id, + }, + }; + }, + }, + session: { + strategy: "database", + maxAge: 60 * 60 * 24 * 7, // 7 days, }, } satisfies NextAuthConfig; diff --git a/src/server/auth/credentials-provider.ts b/src/server/auth/credentials-provider.ts new file mode 100644 index 0000000..22c547d --- /dev/null +++ b/src/server/auth/credentials-provider.ts @@ -0,0 +1,436 @@ +import { createId } from "@paralleldrive/cuid2"; + +export function generateSessionToken() { + return createId(); +} +export const fromDate = (time: number, date = Date.now()) => { + return new Date(date + time * 1000); +}; + +// CredentialsProvider({ +// name: "Credentials", +// credentials: { +// email: { label: "Email", type: "email" }, +// password: { label: "Password", type: "password" }, +// }, +// authorize: async (credentials) => { +// let user: Session["user"] | null = null; + +// if (!credentials?.email || !credentials?.password) return null; + +// if ( +// typeof credentials.password !== "string" || +// typeof credentials.email !== "string" +// ) { +// console.log("WARN: Password or Email is not a string."); +// return null; +// } +// try { +// // Add your own database logic here +// const response = await db.query.users.findFirst({ +// where: eq(users.email, String(credentials.email)), +// }); + +// // No user found +// if (!response || !response.password) { +// if (!response?.password) return null; +// } +// user = response; +// // Check password - using timing-safe comparison via bcrypt +// const isValidPassword = await argon.verify( +// String(response.password), +// String(credentials?.password), +// ); +// if (!isValidPassword) return null; +// console.log("User authenticated successfully:", user.id); + +// return { +// name: user.name, +// email: user.email, +// image: user.image, +// id: user.id, +// role: user.role, +// } as User; +// } catch (e) { +// console.log("WARN: Error while validating credentials."); +// return null; +// } +// }, +// }), + +// callback +// async signIn({ user, account, profile, email, credentials }) { +// console.log("👉 SignIn callback triggered", user?.id); +// console.log("👉 SignIn callback credentials", credentials); +// if (credentials && user.id) await createSession(user.id!); +// // Return true to allow sign-in +// return true; +// }, + +// server action +// export async function login(values: z.infer) { +// return await signIn("credentials", values); +// } +// export async function register(values: z.infer) { +// try { +// const { success } = await api.auth.register({ +// user: { +// email: values.email, +// name: values.name, +// }, +// password: values.password, +// }); +// await signIn("credentials", { +// email: values.email, +// password: values.password, +// }); +// return success; +// } catch (e) { +// return false; +// } +// } + +// export async function createSession(userId: string) { +// if (!adapter.createSession) throw new Error("Adapter not initialized"); +// const sessionToken = generateSessionToken(); +// const sessionExpiry = fromDate(authConfig.session.maxAge); +// console.log("👉 createSession", sessionToken); +// const session = await adapter.createSession({ +// sessionToken: sessionToken, +// userId: userId, +// expires: sessionExpiry, +// }); +// console.log("👉 createSession session", session); +// const cookieStore = await cookies(); +// cookieStore.set("authjs.session-token", sessionToken, { +// expires: sessionExpiry, +// }); +// } + +// register page +// "use client"; +// import { cn } from "@/lib/utils"; +// import { Button } from "@/components/ui/button"; +// import { Card, CardContent } from "@/components/ui/card"; +// import { Input } from "@/components/ui/input"; +// import { zodResolver } from "@hookform/resolvers/zod"; +// import { useForm } from "react-hook-form"; +// import { z } from "zod"; + +// import { +// Form, +// FormControl, +// FormField, +// FormItem, +// FormLabel, +// FormMessage, +// } from "@/components/ui/form"; + +// import { registerSchema } from "@/lib/validation/zod/user"; +// import { appConfig, appRoutes } from "@/config"; +// import Link from "next/link"; +// import { login, register } from "@/server/actions/auth"; +// import { toast } from "sonner"; +// import { AuthProviderList, LeagalFooter } from "."; + +// export function RegisterForm({ +// className, +// ...props +// }: React.ComponentProps<"div">) { +// const form = useForm>({ +// resolver: zodResolver(registerSchema), +// defaultValues: { +// name: "", +// email: "", +// password: "", +// confirmPassword: "", +// }, +// }); + +// // 2. Define a submit handler. +// async function onSubmit(values: z.infer) { +// const success = await register(values); +// if (!success) toast.error("Registrierung fehlgeschlagen"); +// form.reset(); +// } +// return ( +//
+// +// +//
+// +//
+//
+//

Wilkommen

+//

+// Erstelle dein {appConfig.name} Konto +//

+//
+ +// + +//
+// +// Oder mit +// +//
+ +//
+// ( +// +// Name +// +// +// +// +// +// )} +// /> +// ( +// +// Email +// +// +// +// +// +// )} +// /> +// ( +// +// Passwort + +// +// +// +// +// +// )} +// /> +// ( +// +// Passwort Wiederholen +// +// +// +// +// +// )} +// /> +//
+ +// + +//
+// Du hast bereits ein Konto?{" "} +// +// Anmelden +// +//
+//
+//
+// + +//
+// Image +//
+//
+//
+// +//
+// ); +// } + +// Login page + +// export function LoginForm({ +// className, +// ...props +// }: React.ComponentProps<"div">) { +// const form = useForm>({ +// resolver: zodResolver(loginSchema), +// defaultValues: { +// email: "", +// password: "", +// }, +// }); + +// // 2. Define a submit handler. +// async function onSubmit(values: z.infer) { +// const success = await login(values); +// // if (!success) toast.error("Login fehlgeschlagen"); +// // form.reset(); +// } +// return ( +//
+// +// +//
+// +//
+//
+//

Wilkommen Zurück

+//

+// Melde dich in deinem {appConfig.name} Konto an +//

+//
+// +//
+// +// Oder mit +// +//
+//
+// ( +// +// Email +// +// +// +// +// +// )} +// /> +// ( +// +//
+// Passwort + +// +// passwort vergessen? +// +//
+// +// +// +// +//
+// )} +// /> +//
+ +// + +//
+// Du hast noch kein Konto?{" "} +// +// Registrieren +// +//
+//
+//
+// + +//
+// Image +//
+//
+//
+// +//
+// ); +// } + +// export function AuthProviderList() { +// return ( +//
+// +// +// +//
+// ); +// } + +// export function LeagalFooter() { +// return ( +//
+// By clicking continue, you agree to our Terms of Service{" "} +// and Privacy Policy. +//
+// ); +// } diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index f6910a4..5c5bbc3 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -15,7 +15,7 @@ import { User } from "next-auth"; import { type AdapterAccount } from "next-auth/adapters"; import { JSONContent } from "novel"; -export const createTable = pgTableCreator((name) => `wiki-antifa_${name}`); +export const createTable = pgTableCreator((name) => `logipedia_${name}`); export const articles = createTable( "article", @@ -104,6 +104,7 @@ export const users = createTable("user", { withTimezone: true, }).default(sql`CURRENT_TIMESTAMP`), image: varchar("image", { length: 255 }), + password: varchar("password", { length: 255 }), }); export const usersRelations = relations(users, ({ many }) => ({