diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fe72159 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# Recommended to have an example env file for other users know what environment variables need to be set. + +# Get your keys from the Clerk dashboard at https://dashboard.clerk.com +CLERK_SECRET_KEY=sk_ +NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_ +NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in +NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up + +# If using a local docker container (local-database.yml) +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/lockscript?schema=public \ No newline at end of file diff --git a/.gitignore b/.gitignore index 00bba9b..f79bac3 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +#Intellij Editors +.idea diff --git a/README.md b/README.md index dd76392..7370d3e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,47 @@ LockScript Vault is an open-source secure vault for passwords, cards, notes, and - Prisma - Zod +## Getting Started + +Clone the repository +```bash +git clone git@github.com:Lockscript/Lockscript-Vault +``` +```bash +cd Lockscript-Vault +``` + +Repo is using yarn as a package manager. + +[How To Install Yarn](https://classic.yarnpkg.com/lang/en/docs/install) + +Install dependencies +```bash +yarn install +``` + +### Setup local database (if required) + +Start Docker container (requires [Docker](https://docs.docker.com/engine/install/)) + +```bash +docker compose -f ./local-database.yml up -d +``` +Generate database client + +```bash +yarn run generate +``` +```bash +yarn run push +``` + +### Start the dev server +```bash +yarn run dev +``` + + ## How to contribute We accept contributions from the community, but you must follow some rules: diff --git a/local-database.yml b/local-database.yml new file mode 100644 index 0000000..4f92a0d --- /dev/null +++ b/local-database.yml @@ -0,0 +1,17 @@ +# Use postgres/example user/password credentials +version: '3.1' +services: + db: + container_name: lockscript-postgres + image: postgres + restart: always + ports: + - "5432:5432" + environment: + POSTGRES_PASSWORD: postgres + volumes: + - postgres:/var/lib/postgres/data +volumes: + postgres: + external: false + diff --git a/package.json b/package.json index f7f7c92..980b092 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,6 @@ "find:unused": "next-unused" }, "dependencies": { - "@clerk/backend": "^1.23.11", - "@clerk/clerk-sdk-node": "^5.1.6", "@clerk/nextjs": "^6.11.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx index ce338a8..54c9ad4 100644 --- a/src/app/(main)/page.tsx +++ b/src/app/(main)/page.tsx @@ -1,32 +1,19 @@ import {VaultPage} from "@/components/vault/vault-page"; -import prismadb from "@/lib/prismadb"; -import {auth} from "@clerk/nextjs/server"; -import {instantiateVault} from "../actions"; +import {auth, currentUser} from "@clerk/nextjs/server"; +import {getPasswords, instantiateVault} from "../actions"; -export const dynamic = "force-dynamic"; - -const Page = async () => { +export default async function Page(){ const { userId, redirectToSignIn } = await auth(); if (!userId) return redirectToSignIn(); - const user = await prismadb.user.findUnique({ - where: { - id: userId, - }, - include: { - passwordItems: true, - cardItems: true, - pinItems: true, - noteItems: true, - }, - }); + const user = await getPasswords(userId) if (!user) { - instantiateVault(); + const clerkUser = await currentUser() + if(!clerkUser) return redirectToSignIn() + await instantiateVault(clerkUser.id, clerkUser.username as string); } return ; }; - -export default Page; diff --git a/src/app/actions.ts b/src/app/actions.ts index 62335f5..5079619 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -94,19 +94,6 @@ export async function createPasswordItem( throw new Error("Not authenticated"); } - const user = await prismadb.user.findUnique({ - where: { - id: userId, - }, - include: { - passwordItems: true, - }, - }); - - if (!user) { - throw new Error("User not found"); - } - const newPasswordItem = await prismadb.passwordItem.create({ data: { username, @@ -114,26 +101,22 @@ export async function createPasswordItem( password, updatedAt: new Date().toISOString(), createdAt: new Date().toISOString(), - userId: user.id, + user: { + connect: { + id: userId + } + } }, }); return newPasswordItem; } -export async function instantiateVault() { - const { userId } = await auth() - - if (!userId) { - throw new Error("Not authenticated"); - } - - const user = await currentUser() - +export async function instantiateVault(userId: string, username: string) { const vault = await prismadb.user.create({ data: { id: userId, - username: user?.username!, + username: username, }, include: { passwordItems: true, @@ -145,3 +128,17 @@ export async function instantiateVault() { return vault; } + +export async function getPasswords(userId: string) { + return prismadb.user.findUnique({ + where: { + id: userId, + }, + include: { + passwordItems: true, + cardItems: true, + pinItems: true, + noteItems: true, + }, + }); +} diff --git a/src/components/vault/dialogs/create-password-dialog.tsx b/src/components/vault/dialogs/create-password-dialog.tsx index c081a79..83d75b0 100644 --- a/src/components/vault/dialogs/create-password-dialog.tsx +++ b/src/components/vault/dialogs/create-password-dialog.tsx @@ -1,5 +1,3 @@ -"use client" - import {createPasswordItem} from "@/app/actions"; import {Button} from "@/components/ui/button"; import {Dialog,DialogContent,DialogFooter,DialogHeader,DialogTitle} from "@/components/ui/dialog"; @@ -7,21 +5,27 @@ import {Input} from "@/components/ui/input"; import {encrypt} from "@/utils/encryption"; import {useUser} from "@clerk/nextjs"; import {Loader2} from "lucide-react"; -import {useState} from "react"; +import {ChangeEvent, useState} from "react"; import toast from "react-hot-toast"; import {z} from "zod"; +const initialPasswordItemState = { + name: "", + username: "", + website: "", + password: "", +} + export const CreatePasswordDialog = ({ open, - onClose, + onClose, }: { open: boolean; - onClose: () => void; + onClose: ()=> void }) => { - const [name, setName] = useState(""); - const [username, setUsername] = useState(""); - const [website, setWebsite] = useState(""); - const [password, setPassword] = useState(""); + + const [passwordItem, setPasswordItem] = useState(initialPasswordItemState) + const [loading, setLoading] = useState(false); const { user: clerkuser } = useUser(); @@ -47,12 +51,7 @@ export const CreatePasswordDialog = ({ const handleSave = async () => { setLoading(true); - const validationResult = passwordSchema.safeParse({ - name, - username, - website, - password, - }); + const validationResult = passwordSchema.safeParse(passwordItem); if (!validationResult.success) { const errorMessage = @@ -64,11 +63,12 @@ export const CreatePasswordDialog = ({ try { await createPasswordItem( - encrypt(username, clerkuser), - encrypt(website, clerkuser), - encrypt(password, clerkuser) + encrypt(passwordItem.username, clerkuser), + encrypt(passwordItem.website, clerkuser), + encrypt(passwordItem.password, clerkuser) ); toast.success("Password created"); + setPasswordItem(initialPasswordItemState) onClose(); } catch (error) { toast.error("Failed to create password"); @@ -77,6 +77,10 @@ export const CreatePasswordDialog = ({ } }; + const handleChange = (e: ChangeEvent) => { + setPasswordItem(prevState => ({...prevState, [e.target.name]: e.target.value})) + } + return ( @@ -87,50 +91,54 @@ export const CreatePasswordDialog = ({
setName(e.target.value)} + value={passwordItem.name} + onChange={handleChange} maxLength={50} + name="name" /> -
{name.length} / 50
+
{passwordItem.name.length} / 50
setUsername(e.target.value)} + value={passwordItem.username} + onChange={handleChange} maxLength={30} + name="username" />
- {username.length} / 30 + {passwordItem.username.length} / 30
setWebsite(e.target.value)} + value={passwordItem.website} + onChange={handleChange} maxLength={1024} + name="website" />
- {website.length} / 1024 + {passwordItem.website.length} / 1024
setPassword(e.target.value)} + value={passwordItem.password} + onChange={handleChange} type="password" + name="password" maxLength={128} />
- {password.length} / 128 + {passwordItem.password.length} / 128
-
diff --git a/src/utils/encryption.ts b/src/utils/encryption.ts index 7b04a0f..78e031d 100644 --- a/src/utils/encryption.ts +++ b/src/utils/encryption.ts @@ -1,11 +1,13 @@ import CryptoJS from "crypto-js"; + +// this can be reverse engineered - please use a randomly generated string that is saved securely somewhere else. const generateEncryptionPassword = (clerkUser: any) => { if (!clerkUser) return ""; - return `${clerkUser.id}-${clerkUser.createdAt}-${clerkUser.createdAt?.getTime()}-${clerkUser.id.charCodeAt(clerkUser.id.length - 1)}-${clerkUser.createdAt?.getDate()}-${clerkUser.id.charCodeAt(0)}-${clerkUser.createdAt?.getUTCFullYear()}-${clerkUser.id.charCodeAt(1)}-${clerkUser.createdAt?.getUTCHours()}-${clerkUser.id.length}-${clerkUser.createdAt?.getUTCMinutes()}`; }; +// See above comment - the key should be stored securely and used on subsequent encryption calls export const encrypt = (data: string, clerkUser: any) => { const encryptionPassword = generateEncryptionPassword(clerkUser); return CryptoJS.AES.encrypt(data, encryptionPassword).toString(); diff --git a/yarn.lock b/yarn.lock index 7d5909c..3eb6e4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -104,7 +104,7 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" -"@clerk/backend@^1.21.6", "@clerk/backend@^1.23.11": +"@clerk/backend@^1.23.11": version "1.23.11" resolved "https://registry.npmjs.org/@clerk/backend/-/backend-1.23.11.tgz" integrity sha512-N5CYCnVbSVXUkQg9oAAAf9r/kfPmBGxMqjzslDC9Tl3rkXFTkWjkBNnToB6We2ySdrmqiQGR/+/c4mS9XRbaOQ== @@ -124,16 +124,6 @@ "@clerk/types" "^4.45.0" tslib "2.4.1" -"@clerk/clerk-sdk-node@^5.1.6": - version "5.1.6" - resolved "https://registry.npmjs.org/@clerk/clerk-sdk-node/-/clerk-sdk-node-5.1.6.tgz" - integrity sha512-KeF5p0XP0gNoCx+YIHrfrkNNADBz8ZabwPAhOiJZ9Wo14r93WlzRA51IE0Qgteej8IpWwnvKu4/MpiV7FFoLqA== - dependencies: - "@clerk/backend" "^1.21.6" - "@clerk/shared" "^2.20.6" - "@clerk/types" "^4.40.2" - tslib "2.4.1" - "@clerk/nextjs@^6.11.0": version "6.11.0" resolved "https://registry.npmjs.org/@clerk/nextjs/-/nextjs-6.11.0.tgz" @@ -147,7 +137,7 @@ server-only "0.0.1" tslib "2.4.1" -"@clerk/shared@^2.20.18", "@clerk/shared@^2.20.6": +"@clerk/shared@^2.20.18": version "2.20.18" resolved "https://registry.npmjs.org/@clerk/shared/-/shared-2.20.18.tgz" integrity sha512-vSQLZSRFr62+YeE1KE/Xu/eWCrwYJRNA2KRyQYmb2VPleFNmOjtTNlO4xkMZ0eN6BLKxrBo9b0NVwu3L28FhKg== @@ -159,7 +149,7 @@ std-env "^3.7.0" swr "^2.2.0" -"@clerk/types@^4.40.2", "@clerk/types@^4.45.0": +"@clerk/types@^4.45.0": version "4.45.0" resolved "https://registry.npmjs.org/@clerk/types/-/types-4.45.0.tgz" integrity sha512-DSXPWq1xD01tzyHv7CugX2T/XfVUZX2xxQ92cs+JPTrGoqIYm+yjRWqOz1CVJ/76TbYMOrB0efCGOxEcNV/PQw==