├── .eslintrc.json
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── app
├── favicon.ico
├── layout.jsx
└── page.jsx
├── components.json
├── components
├── Cards
│ ├── BankAccountCard.jsx
│ ├── CreditCardCard.jsx
│ ├── EmailAccountCard.jsx
│ ├── IdentificationDocumentCard.jsx
│ ├── NoteCard.jsx
│ ├── SocialMediaAccountCard.jsx
│ ├── WebLoginCard.jsx
│ └── WiFiPasswordCard.jsx
├── Dialogs
│ ├── DeletePasswordDialog.jsx
│ └── PasswordDialog.jsx
├── EmptyState.jsx
├── Forms
│ ├── BankAccountForm.jsx
│ ├── CreditCardForm.jsx
│ ├── EmailAccountForm.jsx
│ ├── IdentificationDocumentForm.jsx
│ ├── NoteForm.jsx
│ ├── PasswordForm.jsx
│ ├── SocialMediaAccountForm.jsx
│ ├── WebLoginForm.jsx
│ └── WiFiPasswordForm.jsx
├── Header.jsx
├── Pages
│ ├── Authentication.jsx
│ └── Dashboard.jsx
├── PasswordGenerator.jsx
├── PasswordStrength.jsx
├── Passwords.jsx
├── SearchBar.jsx
├── SelectMenu.jsx
├── Sidebar.jsx
└── ui
│ ├── accordion.tsx
│ ├── button.tsx
│ ├── dialog.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── progress.tsx
│ ├── select.tsx
│ ├── slider.tsx
│ ├── switch.tsx
│ ├── textarea.tsx
│ ├── toast.tsx
│ ├── toaster.tsx
│ ├── tooltip.tsx
│ └── use-toast.ts
├── constants
└── componentMappings.jsx
├── i18n.js
├── jsconfig.json
├── lib
└── utils.ts
├── locales
├── en
│ └── translation.json
└── tr
│ └── translation.json
├── next.config.js
├── package-lock.json
├── package.json
├── passwords.config.js
├── postcss.config.js
├── screenshots
├── Authentication.png
├── Dashboard.png
├── DeletePasswordDialog.png
├── PasswordDialog.png
└── PasswordGenerator.png
├── src-tauri
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── build.rs
├── icons
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── 32x32.png
│ ├── Square107x107Logo.png
│ ├── Square142x142Logo.png
│ ├── Square150x150Logo.png
│ ├── Square284x284Logo.png
│ ├── Square30x30Logo.png
│ ├── Square310x310Logo.png
│ ├── Square44x44Logo.png
│ ├── Square71x71Logo.png
│ ├── Square89x89Logo.png
│ ├── StoreLogo.png
│ ├── icon.icns
│ ├── icon.ico
│ └── icon.png
├── src
│ └── main.rs
└── tauri.conf.json
├── styles
└── globals.css
├── tailwind.config.js
└── utils
├── addNewPassword.js
├── calculateStrength.js
├── generatePassword.js
├── handleAuthentication.js
├── savePasswords.js
└── updatePassword.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on: workflow_dispatch
3 |
4 | jobs:
5 | release:
6 | permissions:
7 | contents: write
8 | strategy:
9 | fail-fast: false
10 | matrix:
11 | platform: [macos-latest, ubuntu-20.04, windows-latest]
12 | runs-on: ${{ matrix.platform }}
13 |
14 | steps:
15 | - name: Checkout repository
16 | uses: actions/checkout@v3
17 |
18 | - name: Install dependencies (ubuntu only)
19 | if: matrix.platform == 'ubuntu-20.04'
20 | # You can remove libayatana-appindicator3-dev if you don't use the system tray feature.
21 | run: |
22 | sudo apt-get update
23 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
24 |
25 | - name: Rust setup
26 | uses: dtolnay/rust-toolchain@stable
27 |
28 | - name: Rust cache
29 | uses: swatinem/rust-cache@v2
30 | with:
31 | workspaces: "./src-tauri -> target"
32 |
33 | - name: Sync node version and setup cache
34 | uses: actions/setup-node@v3
35 | with:
36 | node-version: "lts/*"
37 | cache: "npm" # Set this to npm, yarn or pnpm.
38 |
39 | - name: Install frontend dependencies
40 | # If you don't have `beforeBuildCommand` configured you may want to build your frontend here too.
41 | run: npm install # Change this to npm, yarn or pnpm.
42 |
43 | - name: Build the app
44 | uses: tauri-apps/tauri-action@v0
45 |
46 | env:
47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 | with:
49 | tagName: ${{ github.ref_name }} # This only works if your workflow triggers on new tags.
50 | releaseName: "v__VERSION__" # tauri-action replaces \_\_VERSION\_\_ with the app version.
51 | releaseBody: "See the assets to download and install this version."
52 | releaseDraft: true
53 | prerelease: false
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore artifacts:
2 | build
3 | coverage
4 | .next
5 | out
6 | src-tauri/target
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2
4 | }
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Mustafa Pekkirişci
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Passwords
2 |
3 | Passwords is an open-source desktop app that simplifies password management, offering a secure and user-friendly experience. Built with Tauri and Next.js.
4 |
5 |
6 |
7 | ## Table of Contents
8 |
9 | - [Features](#features)
10 |
11 | - [Used Technologies](#used-technologies)
12 |
13 | - [How It Works](#how-it-works)
14 |
15 | - [Run Locally](#run-locally)
16 |
17 | - [Contribution](#contribution)
18 |
19 | - [License](#license)
20 |
21 | - [Screenshots](#screenshots)
22 |
23 | - [Download App](#download-app)
24 |
25 | ## Features
26 |
27 | - Access your encrypted password file through the user authentication page. Safeguard your passwords with an additional layer of security using user credentials.
28 |
29 | - Seamlessly add, edit, and delete passwords within the app.
30 |
31 | - Organize your passwords into different types for convenient categorization.
32 |
33 | - Utilize pre-designed forms tailored for various password types, ensuring easy and consistent data entry.
34 |
35 | - Calculate password strength in real-time, assisting you in creating robust and secure passwords.
36 |
37 | - Generate strong and unique passwords based on user preferences and choices. Customize password criteria such as length, character types, and complexity.
38 |
39 | - Enable internationalization with the integration of i18next. Reach a wider audience by providing localized language options, enhancing user experience.
40 |
41 | - Built with Tauri and Next.js, Passwords combines the power of modern web development and desktop application creation. Leverage the advanced capabilities of Tauri for seamless cross-platform deployment.
42 |
43 | - Store your passwords in an encrypted local file, ensuring the safety of your sensitive information. Utilize encryption powered by CryptoJS to maintain the confidentiality of your stored data.
44 |
45 | - Enjoy a modern and user-friendly interface designed for efficiency and ease of use. Access various password types and features with a clean and organized layout.
46 |
47 | ## Used Technologies
48 |
49 | - **Tauri**: Empower your app with the capabilities of Tauri, enabling seamless cross-platform deployment and access to native APIs for enhanced user experience.
50 |
51 | - **Next.js**: Harness the power of Next.js, leveraging its static site generation (SSG) capabilities to create a user interface.
52 |
53 | - **shadcn/ui**: Enhance your app's visual and interactive elements with components from shadcn/ui. These beautifully designed components can be seamlessly integrated into your application, offering accessibility, customizability, and open-source goodness.
54 |
55 | - **CryptoJS**: Utilize CryptoJS for strong encryption, ensuring that sensitive information such as passwords is securely stored within the app's encrypted local file.
56 |
57 | - **i18next**: Implement internationalization with i18next, enabling users to experience the app in their preferred language, enhancing global accessibility.
58 |
59 | - **Lucide React**: Enhance the visual appeal of your app with icons from Lucide React, adding a touch of aesthetic sophistication to the user interface.
60 |
61 | - **Tailwind CSS**: Employ Tailwind CSS for efficient styling, enabling rapid development through its utility-first approach and streamlined design workflow.
62 |
63 | - **Tauri APIs**: Access native functionalities seamlessly through Tauri APIs, providing your app with the potential to interact with the user's system.
64 |
65 | ## How It Works
66 |
67 | Passwords are stored locally in an **encrypted file** on the user's device using **CryptoJS (AES encryption)**, ensuring that sensitive information remains confidential and secure.
68 |
69 | > **Important:** There is **no "forgot password" option**. If you lose your email/password credentials, you will **not** be able to decrypt your password file, and your stored data will be inaccessible. Please ensure you store your credentials safely.
70 |
71 | ### Storage Mechanism
72 |
73 | - Each user's passwords are saved in a unique file named after a **SHA256 hash of their email and password combination**.
74 |
75 | - Files are saved in the system’s **AppData directory** (via Tauri’s `BaseDirectory.AppData`), which varies based on the operating system.
76 |
77 | ### Encryption Flow
78 |
79 | 1. **Authentication**:
80 |
81 | - When a user logs in, their email and password are used to generate a hash-based file name.
82 |
83 | - If the file exists, it's read and **decrypted using AES** with the same email+password as the key.
84 |
85 | - If not, a new encrypted file is created with an empty password array.
86 |
87 | 2. **Saving Passwords**:
88 |
89 | - When passwords are added, edited, or deleted, the updated array is **encrypted using AES** and written back to the same file.
90 |
91 | - AES keys are never stored—encryption is dynamically tied to the email/password pair used during authentication.
92 |
93 | 3. **Encryption Details**:
94 |
95 | - **AES-256** symmetric encryption via CryptoJS.
96 |
97 | - Keys are derived directly from the concatenation of email and password (e.g., `email + password`).
98 |
99 | - Password data is never sent or stored remotely.
100 |
101 | ## Run Locally
102 |
103 | To get a copy of Passwords up and running on your local machine, follow these steps:
104 |
105 | 1. **Install Tauri Prerequisites**:
106 |
107 | Before you begin, make sure you have the required tools and dependencies installed for Tauri. Follow the [Tauri Prerequisites Guide](https://tauri.app/v1/guides/getting-started/prerequisites) to set up your environment.
108 |
109 | 2. **Clone the repository**:
110 |
111 | ```bash
112 | git clone https://github.com/pekkiriscim/passwords.git
113 | ```
114 |
115 | ```bash
116 | cd passwords
117 | ```
118 |
119 | 3. **Install Dependencies**:
120 |
121 | Navigate into the project directory and install the required dependencies:
122 |
123 | ```bash
124 | npm install
125 | ```
126 |
127 | 4. **Run the Development Server**:
128 |
129 | Start the app in development mode with the following command:
130 |
131 | ```bash
132 | npm run tauri dev
133 | ```
134 |
135 | 5. **Build and Launch the Tauri App**:
136 |
137 | To build and launch the Tauri app, run the following command:
138 |
139 | ```bash
140 | npm run tauri build
141 | ```
142 |
143 | ## Contribution
144 |
145 | Contributions to Passwords are welcomed and encouraged! If you're interested in improving the app, adding new features, fixing bugs, or enhancing documentation, your contributions are highly valued.
146 |
147 | ## License
148 |
149 | Passwords is open-source software released under the MIT License.
150 |
151 | The MIT License (MIT) is a permissive open-source license that allows you to use, modify, and distribute the software in your projects, both commercial and non-commercial, while providing attribution to the original authors.
152 |
153 | ## Screenshots
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | ## Download App
166 |
167 | Visit the [Releases](https://github.com/pekkiriscim/passwords/releases) page of the GitHub repository to find the latest version of Passwords. Look for the release version that you want to download.
168 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/app/favicon.ico
--------------------------------------------------------------------------------
/app/layout.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect } from "react";
4 |
5 | import "@/styles/globals.css";
6 |
7 | import { Inter } from "next/font/google";
8 |
9 | import i18n from "@/i18n";
10 |
11 | const inter = Inter({ subsets: ["latin"] });
12 |
13 | export default function RootLayout({ children }) {
14 | useEffect(() => {
15 | const preventContextMenu = (e) => e.preventDefault();
16 |
17 | document.addEventListener("contextmenu", preventContextMenu);
18 |
19 | const userLanguage = window.navigator.language || "en";
20 |
21 | i18n.changeLanguage(userLanguage);
22 |
23 | return () =>
24 | document.removeEventListener("contextmenu", preventContextMenu);
25 | }, []);
26 | return (
27 |
28 |
{children}
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/app/page.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { createContext, useState } from "react";
4 |
5 | import Authentication from "@/components/Pages/Authentication";
6 | import Dashboard from "@/components/Pages/Dashboard";
7 |
8 | import { Toaster } from "@/components/ui/toaster";
9 |
10 | export const AuthContext = createContext();
11 | export const PasswordsContext = createContext();
12 | export const FilterContext = createContext();
13 |
14 | export default function Home() {
15 | const [auth, setAuth] = useState({
16 | email: "",
17 | password: "",
18 | isAuthorized: false,
19 | });
20 |
21 | const [passwords, setPasswords] = useState([]);
22 |
23 | const [filter, setFilter] = useState({
24 | passwordType: "passwords",
25 | filteredPasswords: passwords,
26 | });
27 |
28 | return (
29 |
30 |
33 |
36 | {auth.isAuthorized ? : }
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tailwind": {
6 | "config": "tailwind.config.js",
7 | "css": "styles/globals.css",
8 | "baseColor": "slate",
9 | "cssVariables": true
10 | },
11 | "aliases": {
12 | "components": "@/components",
13 | "utils": "@/lib/utils"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/components/Cards/BankAccountCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { useTranslation } from "react-i18next";
4 |
5 | function BankAccountCard({ password }) {
6 | const [isPasswordVisible, setIsPasswordVisible] = useState(false);
7 |
8 | const { t } = useTranslation();
9 |
10 | return (
11 | <>
12 |
13 | {`${t("dashboard.bank_account_form.iban_label")}:`}
14 |
15 | setIsPasswordVisible(!isPasswordVisible)}
18 | >
19 | {isPasswordVisible ? password.iban : "•".repeat(password.iban.length)}
20 |
21 |
22 |
23 | {`${t("dashboard.bank_account_form.full_name_label")}:`}
24 |
25 | {password.fullName}
26 |
27 | >
28 | );
29 | }
30 |
31 | export default BankAccountCard;
32 |
--------------------------------------------------------------------------------
/components/Cards/CreditCardCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { useTranslation } from "react-i18next";
4 |
5 | function CreditCardCard({ password }) {
6 | const [isPasswordVisible, setIsPasswordVisible] = useState(false);
7 |
8 | const { t } = useTranslation();
9 |
10 | return (
11 | <>
12 |
13 | {`${t("dashboard.credit_card_form.credit_card_number_label")}:`}
14 |
15 | setIsPasswordVisible(!isPasswordVisible)}
18 | >
19 | {isPasswordVisible
20 | ? password.creditCardNumber
21 | : "•".repeat(password.creditCardNumber.length)}
22 |
23 |
24 |
25 | {`${t("dashboard.credit_card_form.card_holder_name_label")}:`}
26 |
27 | {password.cardHolderName}
28 |
29 |
30 | {`${t("dashboard.credit_card_form.expiration_date_label")}:`}
31 |
32 | {password.expirationDate}
33 |
34 |
35 | {`${t("dashboard.credit_card_form.security_code_label")}:`}
36 |
37 | setIsPasswordVisible(!isPasswordVisible)}
40 | >
41 | {isPasswordVisible
42 | ? password.securityCode
43 | : "•".repeat(password.securityCode.length)}
44 |
45 |
46 | >
47 | );
48 | }
49 |
50 | export default CreditCardCard;
51 |
--------------------------------------------------------------------------------
/components/Cards/EmailAccountCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { useTranslation } from "react-i18next";
4 |
5 | function EmailAccountCard({ password }) {
6 | const [isPasswordVisible, setIsPasswordVisible] = useState(false);
7 |
8 | const { t } = useTranslation();
9 |
10 | return (
11 | <>
12 |
13 | {`${t("dashboard.email_account_form.email_label")}:`}
14 |
15 | {password.email}
16 |
17 |
18 | {`${t("dashboard.email_account_form.password_label")}:`}
19 |
20 | setIsPasswordVisible(!isPasswordVisible)}
23 | >
24 | {isPasswordVisible
25 | ? password.password
26 | : "•".repeat(password.password.length)}
27 |
28 |
29 | >
30 | );
31 | }
32 |
33 | export default EmailAccountCard;
34 |
--------------------------------------------------------------------------------
/components/Cards/IdentificationDocumentCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { useTranslation } from "react-i18next";
4 |
5 | function IdentificationDocumentCard({ password }) {
6 | const [isPasswordVisible, setIsPasswordVisible] = useState(false);
7 |
8 | const { t } = useTranslation();
9 |
10 | return (
11 | <>
12 |
13 | {`${t(
14 | "dashboard.identification_document_form.identity_number_label"
15 | )}:`}
16 |
17 | setIsPasswordVisible(!isPasswordVisible)}
20 | >
21 | {isPasswordVisible
22 | ? password.identityNumber
23 | : "•".repeat(password.identityNumber.length)}
24 |
25 |
26 |
27 | {`${t("dashboard.identification_document_form.full_name_label")}:`}
28 |
29 | {password.fullName}
30 |
31 |
32 | {`${t("dashboard.identification_document_form.birth_date_label")}:`}
33 |
34 | {password.birthDate}
35 |
36 |
37 | {`${t("dashboard.identification_document_form.series_number_label")}:`}
38 |
39 | setIsPasswordVisible(!isPasswordVisible)}
42 | >
43 | {isPasswordVisible
44 | ? password.seriesNumber
45 | : "•".repeat(password.seriesNumber.length)}
46 |
47 |
48 | >
49 | );
50 | }
51 |
52 | export default IdentificationDocumentCard;
53 |
--------------------------------------------------------------------------------
/components/Cards/NoteCard.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { useTranslation } from "react-i18next";
4 |
5 | function NoteCard({ password }) {
6 | const { t } = useTranslation();
7 |
8 | return (
9 | <>
10 |
11 | {`${t("dashboard.note_form.note_label")}:`}
12 |
13 | {password.note}
14 |
15 | >
16 | );
17 | }
18 |
19 | export default NoteCard;
20 |
--------------------------------------------------------------------------------
/components/Cards/SocialMediaAccountCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { useTranslation } from "react-i18next";
4 |
5 | function SocialMediaAccountCard({ password }) {
6 | const [isPasswordVisible, setIsPasswordVisible] = useState(false);
7 |
8 | const { t } = useTranslation();
9 |
10 | return (
11 | <>
12 |
13 | {`${t("dashboard.social_media_account_form.username_label")}:`}
14 |
15 | {password.username}
16 |
17 |
18 | {`${t("dashboard.social_media_account_form.password_label")}:`}
19 |
20 | setIsPasswordVisible(!isPasswordVisible)}
23 | >
24 | {isPasswordVisible
25 | ? password.password
26 | : "•".repeat(password.password.length)}
27 |
28 |
29 | >
30 | );
31 | }
32 |
33 | export default SocialMediaAccountCard;
34 |
--------------------------------------------------------------------------------
/components/Cards/WebLoginCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { useTranslation } from "react-i18next";
4 |
5 | function WebLoginCard({ password }) {
6 | const [isPasswordVisible, setIsPasswordVisible] = useState(false);
7 |
8 | const { t } = useTranslation();
9 |
10 | return (
11 | <>
12 |
13 | {`${t("dashboard.web_login_form.email_label")}:`}
14 |
15 | {password.email}
16 |
17 |
18 | {`${t("dashboard.web_login_form.password_label")}:`}
19 |
20 | setIsPasswordVisible(!isPasswordVisible)}
23 | >
24 | {isPasswordVisible
25 | ? password.password
26 | : "•".repeat(password.password.length)}
27 |
28 |
29 |
30 | {`${t("dashboard.web_login_form.url_label")}:`}
31 |
32 | {password.URL}
33 |
34 | >
35 | );
36 | }
37 |
38 | export default WebLoginCard;
39 |
--------------------------------------------------------------------------------
/components/Cards/WiFiPasswordCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { useTranslation } from "react-i18next";
4 |
5 | function WiFiPasswordCard({ password }) {
6 | const [isPasswordVisible, setIsPasswordVisible] = useState(false);
7 |
8 | const { t } = useTranslation();
9 |
10 | return (
11 | <>
12 |
13 | {`${t("dashboard.wifi_password_form.wifi_name_label")}:`}
14 |
15 | {password.wifiName}
16 |
17 |
18 | {`${t("dashboard.wifi_password_form.wifi_password_label")}:`}
19 |
20 | setIsPasswordVisible(!isPasswordVisible)}
23 | >
24 | {isPasswordVisible
25 | ? password.wifiPassword
26 | : "•".repeat(password.wifiPassword.length)}
27 |
28 |
29 | >
30 | );
31 | }
32 |
33 | export default WiFiPasswordCard;
34 |
--------------------------------------------------------------------------------
/components/Dialogs/DeletePasswordDialog.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useContext } from "react";
2 |
3 | import { PasswordsContext, AuthContext } from "@/app/page";
4 |
5 | import {
6 | Dialog,
7 | DialogContent,
8 | DialogDescription,
9 | DialogHeader,
10 | DialogTitle,
11 | DialogTrigger,
12 | } from "@/components/ui/dialog";
13 |
14 | import { Button } from "@/components/ui/button";
15 |
16 | import { Loader2 } from "lucide-react";
17 |
18 | import { savePasswords } from "@/utils/savePasswords";
19 |
20 | import { useTranslation } from "react-i18next";
21 |
22 | function DeletePasswordDialog({ passwordId }) {
23 | const [isDialogOpen, setIsDialogOpen] = useState(false);
24 | const [isLoading, setIsLoading] = useState(false);
25 |
26 | const { passwords, setPasswords } = useContext(PasswordsContext);
27 | const { auth } = useContext(AuthContext);
28 |
29 | const deletePassword = async (e) => {
30 | try {
31 | e.preventDefault();
32 |
33 | setIsLoading(true);
34 |
35 | const newPasswords = await passwords.filter(
36 | (password) => password.passwordId !== passwordId
37 | );
38 |
39 | const newPasswordsState = await savePasswords(
40 | newPasswords,
41 | auth.email,
42 | auth.password
43 | );
44 |
45 | if (newPasswordsState) {
46 | setPasswords(newPasswordsState);
47 |
48 | setIsDialogOpen(false);
49 | }
50 | } catch (error) {
51 | console.log(error);
52 |
53 | return null;
54 | } finally {
55 | setIsLoading(false);
56 | }
57 | };
58 |
59 | const { t } = useTranslation();
60 |
61 | return (
62 |
63 |
64 | {
69 | e.preventDefault();
70 |
71 | setIsDialogOpen(true);
72 | }}
73 | >
74 | {t("dashboard.delete_password_dialog.delete_button")}
75 |
76 |
77 |
78 |
79 |
80 | {t("dashboard.delete_password_dialog.dialog_title_delete")}
81 |
82 |
83 | {t("dashboard.delete_password_dialog.dialog_description_delete")}
84 |
85 |
86 |
87 | {
92 | e.preventDefault();
93 |
94 | setIsDialogOpen(false);
95 | }}
96 | >
97 | {t("dashboard.delete_password_dialog.dialog_cancel_button")}
98 |
99 |
105 | {isLoading ? (
106 |
107 | ) : (
108 | t("dashboard.delete_password_dialog.dialog_delete_button")
109 | )}
110 |
111 |
112 |
113 |
114 | );
115 | }
116 |
117 | export default DeletePasswordDialog;
118 |
--------------------------------------------------------------------------------
/components/Dialogs/PasswordDialog.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, createContext, useContext, useEffect } from "react";
4 |
5 | import { PasswordsContext, AuthContext } from "@/app/page";
6 | import { PasswordDialogContext } from "@/components/Pages/Dashboard";
7 |
8 | import PasswordForm from "@/components/Forms/PasswordForm";
9 |
10 | import { Button } from "@/components/ui/button";
11 |
12 | import {
13 | Dialog,
14 | DialogContent,
15 | DialogDescription,
16 | DialogHeader,
17 | DialogTitle,
18 | DialogTrigger,
19 | } from "@/components/ui/dialog";
20 |
21 | import { Plus, Loader2 } from "lucide-react";
22 |
23 | import { passwordTypeStates } from "@/constants/componentMappings";
24 | import { formComponents } from "@/constants/componentMappings";
25 |
26 | import { updatePassword } from "@/utils/updatePassword";
27 | import { addNewPassword } from "@/utils/addNewPassword";
28 |
29 | import { useTranslation } from "react-i18next";
30 |
31 | export const NewPasswordContext = createContext();
32 |
33 | function PasswordDialog() {
34 | const [newPassword, setNewPassword] = useState();
35 |
36 | const { auth } = useContext(AuthContext);
37 | const { passwords, setPasswords } = useContext(PasswordsContext);
38 |
39 | const { passwordDialog, setPasswordDialog } = useContext(
40 | PasswordDialogContext
41 | );
42 |
43 | useEffect(() => {
44 | if (passwordDialog.isUpdating) {
45 | setNewPassword(passwordDialog.updatePassword);
46 |
47 | setPasswordDialog({ ...passwordDialog, step: 2 });
48 | } else {
49 | setNewPassword(passwordTypeStates.webLogin);
50 |
51 | setPasswordDialog({ ...passwordDialog, step: 1 });
52 | }
53 | }, [passwordDialog.isUpdating, passwordDialog.updatePassword]);
54 |
55 | const handleStep1 = (e) => {
56 | e.preventDefault();
57 |
58 | setPasswordDialog({ ...passwordDialog, step: 1 });
59 | };
60 |
61 | const handleStep2 = (e) => {
62 | e.preventDefault();
63 |
64 | setPasswordDialog({ ...passwordDialog, step: 2 });
65 | };
66 |
67 | const openDialog = (e) => {
68 | e.preventDefault();
69 |
70 | setPasswordDialog({
71 | ...passwordDialog,
72 | isOpen: true,
73 | isUpdating: false,
74 | step: 1,
75 | });
76 | };
77 |
78 | const closeDialog = (e) => {
79 | e.preventDefault();
80 |
81 | if (passwordDialog.isUpdating) {
82 | setNewPassword(passwordDialog.updatePassword);
83 |
84 | setPasswordDialog({ ...passwordDialog, isUpdating: false });
85 | } else {
86 | setNewPassword(passwordTypeStates.webLogin);
87 | }
88 |
89 | setPasswordDialog({ ...passwordDialog, isOpen: false });
90 | };
91 |
92 | const handleSubmit = async (e) => {
93 | try {
94 | e.preventDefault();
95 |
96 | setPasswordDialog({ ...passwordDialog, isLoading: true });
97 |
98 | if (passwordDialog.isUpdating) {
99 | const updatedPasswordsState = await updatePassword(
100 | passwords,
101 | passwordDialog.updatePassword,
102 | newPassword,
103 | auth
104 | );
105 |
106 | if (updatedPasswordsState) {
107 | setPasswords(updatedPasswordsState);
108 |
109 | setNewPassword(passwordDialog.updatePassword);
110 | }
111 | } else {
112 | const newPasswordsState = await addNewPassword(
113 | passwords,
114 | newPassword,
115 | auth
116 | );
117 |
118 | if (newPasswordsState) {
119 | setPasswords(newPasswordsState);
120 |
121 | setPasswordDialog({ ...passwordDialog, step: 1 });
122 |
123 | setNewPassword(passwordTypeStates.webLogin);
124 | }
125 | }
126 | } catch (error) {
127 | console.log(error);
128 |
129 | return null;
130 | } finally {
131 | setPasswordDialog({ ...passwordDialog, isOpen: false, isLoading: false });
132 | }
133 | };
134 |
135 | const { t } = useTranslation();
136 |
137 | return (
138 |
141 |
142 |
143 |
149 |
150 | {t("dashboard.password_dialog.add_new_password_button")}
151 |
152 |
153 |
154 |
155 |
156 | {passwordDialog.isUpdating
157 | ? t("dashboard.password_dialog.dialog_title_edit")
158 | : t("dashboard.password_dialog.dialog_title_add")}
159 |
160 |
161 | {passwordDialog.isUpdating
162 | ? t("dashboard.password_dialog.dialog_description_edit")
163 | : t("dashboard.password_dialog.dialog_description_add")}
164 |
165 |
166 |
202 |
203 |
204 |
205 | );
206 | }
207 |
208 | export default PasswordDialog;
209 |
--------------------------------------------------------------------------------
/components/EmptyState.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { FolderLock } from "lucide-react";
4 |
5 | import { useTranslation } from "react-i18next";
6 |
7 | function EmptyState() {
8 | const { t } = useTranslation();
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | {t("dashboard.empty_state.title")}
17 |
18 |
19 | {t("dashboard.empty_state.description")}
20 |
21 |
22 | );
23 | }
24 |
25 | export default EmptyState;
26 |
--------------------------------------------------------------------------------
/components/Forms/BankAccountForm.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | import { NewPasswordContext } from "@/components/Dialogs/PasswordDialog";
4 |
5 | import { Input } from "@/components/ui/input";
6 | import { Label } from "@/components/ui/label";
7 |
8 | import { useTranslation } from "react-i18next";
9 |
10 | function BankAccountForm() {
11 | const { newPassword, setNewPassword } = useContext(NewPasswordContext);
12 |
13 | const { t } = useTranslation();
14 |
15 | const handleIbanChange = (e) => {
16 | e.preventDefault();
17 |
18 | let formattedIban = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, "");
19 | formattedIban = formattedIban.slice(0, 26);
20 |
21 | let formattedDisplay = t("dashboard.bank_account_form.iban_placeholder");
22 |
23 | for (let i = 2; i < formattedIban.length; i += 4) {
24 | formattedDisplay += " " + formattedIban.slice(i, i + 4);
25 | }
26 |
27 | setNewPassword({ ...newPassword, iban: formattedDisplay });
28 | };
29 |
30 | return (
31 |
65 | );
66 | }
67 |
68 | export default BankAccountForm;
69 |
--------------------------------------------------------------------------------
/components/Forms/CreditCardForm.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | import { NewPasswordContext } from "@/components/Dialogs/PasswordDialog";
4 |
5 | import { Input } from "@/components/ui/input";
6 | import { Label } from "@/components/ui/label";
7 |
8 | import { useTranslation } from "react-i18next";
9 |
10 | function CreditCardForm() {
11 | const { newPassword, setNewPassword } = useContext(NewPasswordContext);
12 |
13 | const { t } = useTranslation();
14 |
15 | const handleCreditCardNumberChange = (e) => {
16 | e.preventDefault();
17 |
18 | let formattedNumber = e.target.value.replace(/[^0-9]/g, "");
19 | formattedNumber = formattedNumber.replace(/\s/g, "");
20 |
21 | let formattedDisplay = "";
22 |
23 | for (let i = 0; i < formattedNumber.length; i += 4) {
24 | formattedDisplay += formattedNumber.slice(i, i + 4) + " ";
25 | }
26 |
27 | setNewPassword({
28 | ...newPassword,
29 | creditCardNumber: formattedDisplay.trim(),
30 | });
31 | };
32 |
33 | const handleExpirationDateChange = (e) => {
34 | e.preventDefault();
35 |
36 | let formattedNumber = e.target.value.replace(/[^0-9]/g, "");
37 | formattedNumber = formattedNumber.replace(/\s/g, "");
38 |
39 | let formattedDisplay = "";
40 |
41 | for (let i = 0; i < formattedNumber.length; i += 2) {
42 | formattedDisplay += formattedNumber.slice(i, i + 2);
43 | if (i + 2 < formattedNumber.length) {
44 | formattedDisplay += "/";
45 | }
46 | }
47 |
48 | setNewPassword({ ...newPassword, expirationDate: formattedDisplay });
49 | };
50 |
51 | const handleSecurityCodeChange = (e) => {
52 | e.preventDefault();
53 |
54 | let formattedSecurityCode = e.target.value.replace(/[^0-9]/g, "");
55 |
56 | setNewPassword({ ...newPassword, securityCode: formattedSecurityCode });
57 | };
58 |
59 | return (
60 |
61 |
62 |
63 | {t("dashboard.credit_card_form.credit_card_number_label")}
64 |
65 |
78 |
79 |
80 |
81 | {t("dashboard.credit_card_form.card_holder_name_label")}
82 |
83 |
94 | setNewPassword({ ...newPassword, cardHolderName: e.target.value })
95 | }
96 | />
97 |
98 |
136 |
137 | );
138 | }
139 |
140 | export default CreditCardForm;
141 |
--------------------------------------------------------------------------------
/components/Forms/EmailAccountForm.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useContext } from "react";
2 |
3 | import PasswordStrength from "@/components/PasswordStrength";
4 | import PasswordGenerator from "@/components/PasswordGenerator";
5 | import { NewPasswordContext } from "@/components/Dialogs/PasswordDialog";
6 |
7 | import { Input } from "@/components/ui/input";
8 | import { Label } from "@/components/ui/label";
9 | import { Button } from "@/components/ui/button";
10 |
11 | import { Eye, EyeOff } from "lucide-react";
12 |
13 | import { useTranslation } from "react-i18next";
14 |
15 | function EmailAccountForm() {
16 | const [isPasswordVisible, setIsPasswordVisible] = useState(false);
17 |
18 | const { newPassword, setNewPassword } = useContext(NewPasswordContext);
19 |
20 | const { t } = useTranslation();
21 |
22 | return (
23 |
24 |
25 |
26 | {t("dashboard.email_account_form.email_label")}
27 |
28 |
37 | setNewPassword({ ...newPassword, email: e.target.value })
38 | }
39 | />
40 |
41 |
42 |
43 | {t("dashboard.email_account_form.password_label")}
44 |
45 |
46 |
55 | setNewPassword({ ...newPassword, password: e.target.value })
56 | }
57 | />
58 | {
63 | e.preventDefault();
64 |
65 | setIsPasswordVisible(!isPasswordVisible);
66 | }}
67 | >
68 | {isPasswordVisible ? (
69 |
70 | ) : (
71 |
72 | )}
73 |
74 |
75 |
76 |
77 |
78 |
79 | );
80 | }
81 |
82 | export default EmailAccountForm;
83 |
--------------------------------------------------------------------------------
/components/Forms/IdentificationDocumentForm.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | import { NewPasswordContext } from "@/components/Dialogs/PasswordDialog";
4 |
5 | import { Input } from "@/components/ui/input";
6 | import { Label } from "@/components/ui/label";
7 |
8 | import { useTranslation } from "react-i18next";
9 |
10 | function IdentificationDocumentForm() {
11 | const { newPassword, setNewPassword } = useContext(NewPasswordContext);
12 |
13 | const { t } = useTranslation();
14 |
15 | const handleIdentityNumberChange = (e) => {
16 | e.preventDefault();
17 |
18 | let formattedIdentityNumber = e.target.value.replace(/[^0-9]/g, "");
19 |
20 | setNewPassword({ ...newPassword, identityNumber: formattedIdentityNumber });
21 | };
22 |
23 | const handleBirthDateChange = (e) => {
24 | e.preventDefault();
25 |
26 | let formattedBirthDate = e.target.value.replace(/[^0-9]/g, "");
27 | formattedBirthDate = formattedBirthDate.slice(0, 8);
28 |
29 | let formattedDisplay = "";
30 | formattedDisplay += formattedBirthDate.slice(0, 2);
31 |
32 | if (formattedBirthDate.length > 2) {
33 | formattedDisplay += "/" + formattedBirthDate.slice(2, 4);
34 | }
35 |
36 | if (formattedBirthDate.length > 4) {
37 | formattedDisplay += "/" + formattedBirthDate.slice(4, 8);
38 | }
39 |
40 | setNewPassword({ ...newPassword, birthDate: formattedDisplay });
41 | };
42 |
43 | return (
44 |
45 |
46 |
47 | {t("dashboard.identification_document_form.identity_number_label")}
48 |
49 |
61 |
62 |
63 |
64 | {t("dashboard.identification_document_form.full_name_label")}
65 |
66 |
77 | setNewPassword({ ...newPassword, fullName: e.target.value })
78 | }
79 | />
80 |
81 |
119 |
120 | );
121 | }
122 |
123 | export default IdentificationDocumentForm;
124 |
--------------------------------------------------------------------------------
/components/Forms/NoteForm.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | import { NewPasswordContext } from "@/components/Dialogs/PasswordDialog";
4 |
5 | import { Textarea } from "@/components/ui/textarea";
6 | import { Label } from "@/components/ui/label";
7 |
8 | import { useTranslation } from "react-i18next";
9 |
10 | function NoteForm() {
11 | const { newPassword, setNewPassword } = useContext(NewPasswordContext);
12 |
13 | const { t } = useTranslation();
14 |
15 | return (
16 |
17 |
18 | {t("dashboard.note_form.note_label")}
19 |
30 |
31 | );
32 | }
33 |
34 | export default NoteForm;
35 |
--------------------------------------------------------------------------------
/components/Forms/PasswordForm.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | import SelectMenu from "@/components/SelectMenu";
4 |
5 | import { NewPasswordContext } from "@/components/Dialogs/PasswordDialog";
6 |
7 | import { Label } from "@/components//ui/label";
8 | import { Input } from "@/components//ui/input";
9 |
10 | import { passwordTypeStates } from "@/constants/componentMappings";
11 |
12 | import { useTranslation } from "react-i18next";
13 |
14 | function PasswordForm() {
15 | const { newPassword, setNewPassword } = useContext(NewPasswordContext);
16 |
17 | const { t } = useTranslation();
18 |
19 | const passwordTypeOptions = [
20 | {
21 | value: "webLogin",
22 | text: t("dashboard.password_form.password_types.webLogin"),
23 | },
24 | {
25 | value: "creditCard",
26 | text: t("dashboard.password_form.password_types.creditCard"),
27 | },
28 | {
29 | value: "identificationDocument",
30 | text: t("dashboard.password_form.password_types.identificationDocument"),
31 | },
32 | {
33 | value: "note",
34 | text: t("dashboard.password_form.password_types.note"),
35 | },
36 | {
37 | value: "socialMediaAccount",
38 | text: t("dashboard.password_form.password_types.socialMediaAccount"),
39 | },
40 | {
41 | value: "emailAccount",
42 | text: t("dashboard.password_form.password_types.emailAccount"),
43 | },
44 | {
45 | value: "wiFiPassword",
46 | text: t("dashboard.password_form.password_types.wiFiPassword"),
47 | },
48 | {
49 | value: "bankAccount",
50 | text: t("dashboard.password_form.password_types.bankAccount"),
51 | },
52 | ];
53 |
54 | return (
55 | <>
56 | setNewPassword(passwordTypeStates[string])}
63 | />
64 |
65 |
66 | {t("dashboard.password_form.password_title_label")}
67 |
68 |
77 | setNewPassword({ ...newPassword, passwordTitle: e.target.value })
78 | }
79 | />
80 |
81 | >
82 | );
83 | }
84 |
85 | export default PasswordForm;
86 |
--------------------------------------------------------------------------------
/components/Forms/SocialMediaAccountForm.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useContext } from "react";
2 |
3 | import PasswordStrength from "@/components/PasswordStrength";
4 | import PasswordGenerator from "@/components/PasswordGenerator";
5 | import { NewPasswordContext } from "@/components/Dialogs/PasswordDialog";
6 |
7 | import { Input } from "@/components/ui/input";
8 | import { Label } from "@/components/ui/label";
9 | import { Button } from "@/components/ui/button";
10 |
11 | import { Eye, EyeOff } from "lucide-react";
12 |
13 | import { useTranslation } from "react-i18next";
14 |
15 | function SocialMediaAccountForm() {
16 | const [isPasswordVisible, setIsPasswordVisible] = useState(false);
17 |
18 | const { newPassword, setNewPassword } = useContext(NewPasswordContext);
19 |
20 | const { t } = useTranslation();
21 |
22 | return (
23 |
24 |
25 |
26 | {t("dashboard.social_media_account_form.username_label")}
27 |
28 |
39 | setNewPassword({ ...newPassword, username: e.target.value })
40 | }
41 | />
42 |
43 |
44 |
45 | {t("dashboard.social_media_account_form.password_label")}
46 |
47 |
48 |
59 | setNewPassword({ ...newPassword, password: e.target.value })
60 | }
61 | />
62 | {
67 | e.preventDefault();
68 |
69 | setIsPasswordVisible(!isPasswordVisible);
70 | }}
71 | >
72 | {isPasswordVisible ? (
73 |
74 | ) : (
75 |
76 | )}
77 |
78 |
79 |
80 |
81 |
82 |
83 | );
84 | }
85 |
86 | export default SocialMediaAccountForm;
87 |
--------------------------------------------------------------------------------
/components/Forms/WebLoginForm.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useContext } from "react";
2 |
3 | import PasswordStrength from "@/components/PasswordStrength";
4 | import PasswordGenerator from "@/components/PasswordGenerator";
5 | import { NewPasswordContext } from "@/components/Dialogs/PasswordDialog";
6 |
7 | import { Input } from "@/components/ui/input";
8 | import { Label } from "@/components/ui/label";
9 | import { Button } from "@/components/ui/button";
10 |
11 | import { Eye, EyeOff } from "lucide-react";
12 |
13 | import { useTranslation } from "react-i18next";
14 |
15 | function WebLoginForm() {
16 | const [isPasswordVisible, setIsPasswordVisible] = useState(false);
17 |
18 | const { newPassword, setNewPassword } = useContext(NewPasswordContext);
19 |
20 | const { t } = useTranslation();
21 |
22 | return (
23 |
24 |
25 |
26 | {t("dashboard.web_login_form.email_label")}
27 |
28 |
37 | setNewPassword({ ...newPassword, email: e.target.value })
38 | }
39 | />
40 |
41 |
42 |
43 | {t("dashboard.web_login_form.password_label")}
44 |
45 |
46 |
55 | setNewPassword({ ...newPassword, password: e.target.value })
56 | }
57 | />
58 | {
63 | e.preventDefault();
64 |
65 | setIsPasswordVisible(!isPasswordVisible);
66 | }}
67 | >
68 | {isPasswordVisible ? (
69 |
70 | ) : (
71 |
72 | )}
73 |
74 |
75 |
76 |
77 |
78 |
79 | {t("dashboard.web_login_form.url_label")}
80 |
89 | setNewPassword({ ...newPassword, URL: e.target.value })
90 | }
91 | />
92 |
93 |
94 | );
95 | }
96 |
97 | export default WebLoginForm;
98 |
--------------------------------------------------------------------------------
/components/Forms/WiFiPasswordForm.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useContext } from "react";
2 |
3 | import PasswordStrength from "@/components/PasswordStrength";
4 | import PasswordGenerator from "@/components/PasswordGenerator";
5 | import { NewPasswordContext } from "@/components/Dialogs/PasswordDialog";
6 |
7 | import { Input } from "@/components/ui/input";
8 | import { Label } from "@/components/ui/label";
9 | import { Button } from "@/components/ui/button";
10 |
11 | import { Eye, EyeOff } from "lucide-react";
12 |
13 | import { useTranslation } from "react-i18next";
14 |
15 | function WiFiPasswordForm() {
16 | const [isPasswordVisible, setIsPasswordVisible] = useState(false);
17 |
18 | const { newPassword, setNewPassword } = useContext(NewPasswordContext);
19 |
20 | const { t } = useTranslation();
21 |
22 | return (
23 |
24 |
25 |
26 | {t("dashboard.wifi_password_form.wifi_name_label")}
27 |
28 |
37 | setNewPassword({ ...newPassword, wifiName: e.target.value })
38 | }
39 | />
40 |
41 |
42 |
43 | {t("dashboard.wifi_password_form.wifi_password_label")}
44 |
45 |
46 |
57 | setNewPassword({ ...newPassword, wifiPassword: e.target.value })
58 | }
59 | />
60 | {
65 | e.preventDefault();
66 |
67 | setIsPasswordVisible(!isPasswordVisible);
68 | }}
69 | >
70 | {isPasswordVisible ? (
71 |
72 | ) : (
73 |
74 | )}
75 |
76 |
77 |
78 |
79 |
80 |
81 | );
82 | }
83 |
84 | export default WiFiPasswordForm;
85 |
--------------------------------------------------------------------------------
/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | import { FilterContext } from "@/app/page";
4 |
5 | import { useTranslation } from "react-i18next";
6 |
7 | function Header() {
8 | const { filter } = useContext(FilterContext);
9 |
10 | const { t } = useTranslation();
11 |
12 | const passwordTypes = {
13 | passwords: t("dashboard.header.password_types.passwords"),
14 | webLogin: t("dashboard.header.password_types.webLogin"),
15 | creditCard: t("dashboard.header.password_types.creditCard"),
16 | identificationDocument: t(
17 | "dashboard.header.password_types.identificationDocument"
18 | ),
19 | note: t("dashboard.header.password_types.note"),
20 | socialMediaAccount: t("dashboard.header.password_types.socialMediaAccount"),
21 | emailAccount: t("dashboard.header.password_types.emailAccount"),
22 | wiFiPassword: t("dashboard.header.password_types.wiFiPassword"),
23 | bankAccount: t("dashboard.header.password_types.bankAccount"),
24 | };
25 |
26 | return (
27 |
28 |
29 | {passwordTypes[filter.passwordType]}
30 |
31 |
32 | {t("dashboard.header.description")}
33 |
34 |
35 | );
36 | }
37 |
38 | export default Header;
39 |
--------------------------------------------------------------------------------
/components/Pages/Authentication.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useState } from "react";
2 |
3 | import { AuthContext, PasswordsContext } from "@/app/page";
4 |
5 | import { Label } from "@/components/ui/label";
6 | import { Input } from "@/components/ui/input";
7 | import { Button } from "@/components/ui/button";
8 |
9 | import { Loader2, Lock } from "lucide-react";
10 |
11 | import { handleAuthentication } from "@/utils/handleAuthentication";
12 |
13 | import { useTranslation } from "react-i18next";
14 |
15 | import { useToast } from "@/components/ui/use-toast";
16 |
17 | function Authentication() {
18 | const [isLoading, setIsLoading] = useState(false);
19 |
20 | const { auth, setAuth } = useContext(AuthContext);
21 | const { setPasswords } = useContext(PasswordsContext);
22 |
23 | const { t } = useTranslation();
24 |
25 | const { toast } = useToast();
26 |
27 | const handleSubmit = async (e) => {
28 | try {
29 | e.preventDefault();
30 |
31 | setIsLoading(true);
32 |
33 | const passwords = await handleAuthentication(auth.email, auth.password);
34 |
35 | if (!passwords) {
36 | toast({
37 | title: t("authentication.form.error_title"),
38 | description: t("authentication.form.error_description"),
39 | });
40 |
41 | return null;
42 | }
43 |
44 | setPasswords(passwords);
45 |
46 | setAuth({ ...auth, isAuthorized: true });
47 | } catch (error) {
48 | console.log(error);
49 | } finally {
50 | setIsLoading(false);
51 | }
52 | };
53 |
54 | return (
55 |
112 | );
113 | }
114 |
115 | export default Authentication;
116 |
--------------------------------------------------------------------------------
/components/Pages/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useState } from "react";
2 |
3 | import Header from "@/components/Header";
4 | import Sidebar from "@/components/Sidebar";
5 | import SearchBar from "@/components/SearchBar";
6 | import Passwords from "@/components/Passwords";
7 |
8 | export const PasswordDialogContext = createContext();
9 |
10 | function Dashboard() {
11 | const [passwordDialog, setPasswordDialog] = useState({
12 | isOpen: false,
13 | isUpdating: false,
14 | updatePassword: null,
15 | isLoading: false,
16 | step: null,
17 | });
18 |
19 | return (
20 |
26 |
34 |
35 | );
36 | }
37 |
38 | export default Dashboard;
39 |
--------------------------------------------------------------------------------
/components/PasswordGenerator.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useContext } from "react";
2 |
3 | import { NewPasswordContext } from "@/components/Dialogs/PasswordDialog";
4 |
5 | import { Button } from "@/components/ui/button";
6 | import { Label } from "@/components//ui/label";
7 | import { Slider } from "@/components/ui/slider";
8 | import { Switch } from "@/components/ui/switch";
9 | import { useToast } from "@/components/ui/use-toast";
10 |
11 | import {
12 | Tooltip,
13 | TooltipContent,
14 | TooltipProvider,
15 | TooltipTrigger,
16 | } from "@/components/ui/tooltip";
17 |
18 | import { Sparkles } from "lucide-react";
19 |
20 | import { generatePassword } from "@/utils/generatePassword";
21 |
22 | import { useTranslation } from "react-i18next";
23 |
24 | function PasswordGenerator({ passwordName }) {
25 | const [generateSettings, setGenerateSettings] = useState({
26 | length: 8,
27 | includeUpperCase: true,
28 | includeLowerCase: true,
29 | includeDigits: true,
30 | includeSpecialChars: true,
31 | });
32 |
33 | const { newPassword, setNewPassword } = useContext(NewPasswordContext);
34 |
35 | const { toast } = useToast();
36 |
37 | const { t } = useTranslation();
38 |
39 | const handleGeneratePassword = (e) => {
40 | e.preventDefault();
41 |
42 | const generatedPassword = generatePassword(generateSettings);
43 |
44 | if (!generatedPassword) {
45 | toast({
46 | title: t("dashboard.password_generator.error_title"),
47 | description: t("dashboard.password_generator.error_description"),
48 | });
49 | } else {
50 | setNewPassword({
51 | ...newPassword,
52 | [passwordName]: generatedPassword,
53 | });
54 | }
55 | };
56 |
57 | return (
58 |
59 |
60 |
61 |
67 |
68 | {t("dashboard.password_generator.button")}
69 |
70 |
71 |
75 |
76 |
77 |
78 | {t("dashboard.password_generator.length")}
79 |
80 |
{generateSettings.length}
81 |
82 |
88 | setGenerateSettings({ ...generateSettings, length: number })
89 | }
90 | />
91 |
92 |
93 |
94 | {t("dashboard.password_generator.lowercase")}
95 |
96 |
100 | setGenerateSettings({
101 | ...generateSettings,
102 | includeLowerCase: boolean,
103 | })
104 | }
105 | />
106 |
107 |
108 |
109 | {t("dashboard.password_generator.uppercase")}
110 |
111 |
115 | setGenerateSettings({
116 | ...generateSettings,
117 | includeUpperCase: boolean,
118 | })
119 | }
120 | />
121 |
122 |
123 |
124 | {t("dashboard.password_generator.digit")}
125 |
126 |
130 | setGenerateSettings({
131 | ...generateSettings,
132 | includeDigits: boolean,
133 | })
134 | }
135 | />
136 |
137 |
138 |
139 | {t("dashboard.password_generator.special_character")}
140 |
141 |
145 | setGenerateSettings({
146 | ...generateSettings,
147 | includeSpecialChars: boolean,
148 | })
149 | }
150 | />
151 |
152 |
153 |
154 |
155 | );
156 | }
157 |
158 | export default PasswordGenerator;
159 |
--------------------------------------------------------------------------------
/components/PasswordStrength.jsx:
--------------------------------------------------------------------------------
1 | import { Progress } from "@/components/ui/progress";
2 |
3 | import { CheckCircle2, XCircle } from "lucide-react";
4 |
5 | import { calculateStrength } from "@/utils/calculateStrength";
6 |
7 | import { useTranslation } from "react-i18next";
8 |
9 | function PasswordStrength({ password }) {
10 | const passwordStrength = calculateStrength(password);
11 |
12 | const renderPasswordRequirement = (condition, text) => (
13 |
14 | {condition ? (
15 |
16 | ) : (
17 |
18 | )}
19 |
20 | {text}
21 |
22 |
23 | );
24 |
25 | const { t } = useTranslation();
26 |
27 | return (
28 | <>
29 |
30 |
31 | {renderPasswordRequirement(
32 | passwordStrength.isLengthSufficient,
33 | t("dashboard.password_strength.is_length_sufficient")
34 | )}
35 | {renderPasswordRequirement(
36 | passwordStrength.hasLowercase,
37 | t("dashboard.password_strength.has_lowercase")
38 | )}
39 | {renderPasswordRequirement(
40 | passwordStrength.hasUppercase,
41 | t("dashboard.password_strength.has_uppercase")
42 | )}
43 | {renderPasswordRequirement(
44 | passwordStrength.hasDigit,
45 | t("dashboard.password_strength.has_digit")
46 | )}
47 | {renderPasswordRequirement(
48 | passwordStrength.hasSpecialCharacter,
49 | t("dashboard.password_strength.has_special_character")
50 | )}
51 |
52 | >
53 | );
54 | }
55 |
56 | export default PasswordStrength;
57 |
--------------------------------------------------------------------------------
/components/Passwords.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | import { FilterContext } from "@/app/page";
4 | import { PasswordDialogContext } from "@/components/Pages/Dashboard";
5 |
6 | import DeletePasswordDialog from "@/components/Dialogs/DeletePasswordDialog";
7 | import EmptyState from "@/components/EmptyState";
8 |
9 | import { Button } from "@/components/ui/button";
10 |
11 | import {
12 | Accordion,
13 | AccordionContent,
14 | AccordionItem,
15 | AccordionTrigger,
16 | } from "@/components/ui/accordion";
17 |
18 | import { passwordIcons } from "@/constants/componentMappings";
19 |
20 | import { useTranslation } from "react-i18next";
21 | import { cardComponents } from "@/constants/componentMappings";
22 |
23 | function Passwords() {
24 | const { filter } = useContext(FilterContext);
25 |
26 | const { passwordDialog, setPasswordDialog } = useContext(
27 | PasswordDialogContext
28 | );
29 |
30 | const { t } = useTranslation();
31 |
32 | return (
33 |
34 |
35 | {filter.filteredPasswords.length === 0 ? (
36 |
37 | ) : (
38 | filter.filteredPasswords
39 | .sort((a, b) => b.passwordId - a.passwordId)
40 | .map((password, index) => {
41 | const IconComponent = passwordIcons[password.passwordType];
42 | const CardComponent = cardComponents[password.passwordType];
43 |
44 | return (
45 |
46 |
47 |
48 |
49 | {password.passwordTitle}
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | {
63 | e.preventDefault();
64 |
65 | setPasswordDialog({
66 | ...passwordDialog,
67 | isOpen: true,
68 | isUpdating: true,
69 | updatePassword: password,
70 | });
71 | }}
72 | >
73 | {t("dashboard.passwords.edit_button")}
74 |
75 |
76 |
77 |
78 | );
79 | })
80 | )}
81 |
82 |
83 | );
84 | }
85 |
86 | export default Passwords;
87 |
--------------------------------------------------------------------------------
/components/SearchBar.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useState } from "react";
2 |
3 | import { FilterContext, PasswordsContext } from "@/app/page";
4 |
5 | import PasswordDialog from "@/components/Dialogs/PasswordDialog";
6 |
7 | import { Input } from "@/components/ui/input";
8 |
9 | import { useTranslation } from "react-i18next";
10 |
11 | function SearchBar() {
12 | const [search, setSearch] = useState("");
13 |
14 | const { passwords } = useContext(PasswordsContext);
15 | const { filter, setFilter } = useContext(FilterContext);
16 |
17 | const { t } = useTranslation();
18 |
19 | useEffect(() => {
20 | if (search === "") {
21 | let filteredPasswords;
22 |
23 | if (filter.passwordType === "passwords") {
24 | filteredPasswords = passwords;
25 | } else {
26 | filteredPasswords = passwords.filter(
27 | (element) => element.passwordType === filter.passwordType
28 | );
29 | }
30 |
31 | setFilter({ ...filter, filteredPasswords });
32 | } else {
33 | if (filter.passwordType === "passwords") {
34 | const filteredPasswords = passwords.filter((element) =>
35 | element.passwordTitle.toLowerCase().includes(search.toLowerCase())
36 | );
37 |
38 | setFilter({ ...filter, filteredPasswords });
39 | } else {
40 | const filteredPasswords = passwords
41 | .filter((element) => element.passwordType === filter.passwordType)
42 | .filter((element) =>
43 | element.passwordTitle.toLowerCase().includes(search.toLowerCase())
44 | );
45 |
46 | setFilter({ ...filter, filteredPasswords });
47 | }
48 | }
49 | }, [search, passwords, filter.passwordType]);
50 |
51 | return (
52 |
53 |
setSearch(e.target.value)}
60 | />
61 |
62 |
63 | );
64 | }
65 |
66 | export default SearchBar;
67 |
--------------------------------------------------------------------------------
/components/SelectMenu.jsx:
--------------------------------------------------------------------------------
1 | import { Label } from "@/components/ui/label";
2 |
3 | import {
4 | Select,
5 | SelectContent,
6 | SelectGroup,
7 | SelectItem,
8 | SelectTrigger,
9 | SelectValue,
10 | } from "@/components/ui/select";
11 |
12 | function SelectMenu({ value, onValueChange, data, label, id, placeholder }) {
13 | return (
14 |
15 | {label}
16 |
17 |
18 |
19 |
20 |
21 |
22 | {data.map((element, index) => (
23 |
24 | {element.text}
25 |
26 | ))}
27 |
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | export default SelectMenu;
35 |
--------------------------------------------------------------------------------
/components/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect } from "react";
2 |
3 | import { FilterContext, PasswordsContext } from "@/app/page";
4 |
5 | import { Button } from "@/components/ui/button";
6 |
7 | import {
8 | Lock,
9 | Globe,
10 | CreditCard,
11 | FileText,
12 | AtSign,
13 | Mail,
14 | Wifi,
15 | Landmark,
16 | UserCircle2,
17 | } from "lucide-react";
18 |
19 | import { useTranslation } from "react-i18next";
20 |
21 | function Sidebar() {
22 | const { filter, setFilter } = useContext(FilterContext);
23 | const { passwords } = useContext(PasswordsContext);
24 |
25 | useEffect(() => {
26 | if (filter.passwordType === "passwords") {
27 | setFilter({ ...filter, filteredPasswords: passwords });
28 | } else {
29 | const filteredPasswords = passwords.filter(
30 | (element) => element.passwordType === filter.passwordType
31 | );
32 |
33 | setFilter({ ...filter, filteredPasswords: filteredPasswords });
34 | }
35 | }, [filter.passwordType, passwords]);
36 |
37 | const { t } = useTranslation();
38 |
39 | const sidebarContents = {
40 | passwords: {
41 | name: t("dashboard.sidebar.passwords"),
42 | children: {
43 | passwords: {
44 | name: t("dashboard.sidebar.passwords_children.passwords"),
45 | value: "passwords",
46 | icon: ,
47 | },
48 | },
49 | },
50 | categories: {
51 | name: t("dashboard.sidebar.categories"),
52 | children: {
53 | webLogins: {
54 | name: t("dashboard.sidebar.categories_children.webLogins"),
55 | value: "webLogin",
56 | icon: ,
57 | },
58 | creditCards: {
59 | name: t("dashboard.sidebar.categories_children.creditCards"),
60 | value: "creditCard",
61 | icon: ,
62 | },
63 | identificationDocuments: {
64 | name: t(
65 | "dashboard.sidebar.categories_children.identificationDocuments"
66 | ),
67 | value: "identificationDocument",
68 | icon: ,
69 | },
70 | notes: {
71 | name: t("dashboard.sidebar.categories_children.notes"),
72 | value: "note",
73 | icon: ,
74 | },
75 | socialMediaAccounts: {
76 | name: t("dashboard.sidebar.categories_children.socialMediaAccounts"),
77 | value: "socialMediaAccount",
78 | icon: ,
79 | },
80 | emailAccounts: {
81 | name: t("dashboard.sidebar.categories_children.emailAccounts"),
82 | value: "emailAccount",
83 | icon: ,
84 | },
85 | wiFiPasswords: {
86 | name: t("dashboard.sidebar.categories_children.wiFiPasswords"),
87 | value: "wiFiPassword",
88 | icon: ,
89 | },
90 | bankAccounts: {
91 | name: t("dashboard.sidebar.categories_children.bankAccounts"),
92 | value: "bankAccount",
93 | icon: ,
94 | },
95 | },
96 | },
97 | };
98 |
99 | return (
100 |
101 | {Object.values(sidebarContents).map((element, index) => (
102 |
103 |
104 | {element.name}
105 |
106 |
107 | {Object.values(element.children).map((element, index) => (
108 | {
116 | e.preventDefault();
117 |
118 | setFilter({ ...filter, passwordType: element.value });
119 | }}
120 | >
121 | {element.icon}
122 | {element.name}
123 |
124 | ))}
125 |
126 |
127 | ))}
128 |
129 | );
130 | }
131 |
132 | export default Sidebar;
133 |
--------------------------------------------------------------------------------
/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
5 | import { ChevronDown } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Accordion = AccordionPrimitive.Root;
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ));
21 | AccordionItem.displayName = "AccordionItem";
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ));
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
55 | {children}
56 |
57 | ));
58 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
59 |
60 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
61 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "underline-offset-4 hover:underline text-primary",
21 | },
22 | size: {
23 | default: "h-10 py-2 px-4",
24 | sm: "h-9 px-3 rounded-md",
25 | lg: "h-11 px-8 rounded-md",
26 | },
27 | },
28 | defaultVariants: {
29 | variant: "default",
30 | size: "default",
31 | },
32 | }
33 | );
34 |
35 | export interface ButtonProps
36 | extends React.ButtonHTMLAttributes,
37 | VariantProps {
38 | asChild?: boolean;
39 | }
40 |
41 | const Button = React.forwardRef(
42 | ({ className, variant, size, asChild = false, ...props }, ref) => {
43 | const Comp = asChild ? Slot : "button";
44 | return (
45 |
50 | );
51 | }
52 | );
53 | Button.displayName = "Button";
54 |
55 | export { Button, buttonVariants };
56 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DialogPrimitive from "@radix-ui/react-dialog";
5 | import { X } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = ({
14 | className,
15 | children,
16 | ...props
17 | }: DialogPrimitive.DialogPortalProps) => (
18 |
19 |
20 | {children}
21 |
22 |
23 | );
24 | DialogPortal.displayName = DialogPrimitive.Portal.displayName;
25 |
26 | const DialogOverlay = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ));
39 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
40 |
41 | const DialogContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
46 |
47 |
55 | {children}
56 |
57 |
58 | ));
59 | DialogContent.displayName = DialogPrimitive.Content.displayName;
60 |
61 | const DialogHeader = ({
62 | className,
63 | ...props
64 | }: React.HTMLAttributes) => (
65 |
72 | );
73 | DialogHeader.displayName = "DialogHeader";
74 |
75 | const DialogFooter = ({
76 | className,
77 | ...props
78 | }: React.HTMLAttributes) => (
79 |
86 | );
87 | DialogFooter.displayName = "DialogFooter";
88 |
89 | const DialogTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
101 | ));
102 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
103 |
104 | const DialogDescription = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ className, ...props }, ref) => (
108 |
113 | ));
114 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
115 |
116 | export {
117 | Dialog,
118 | DialogTrigger,
119 | DialogContent,
120 | DialogHeader,
121 | DialogFooter,
122 | DialogTitle,
123 | DialogDescription,
124 | };
125 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | }
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as LabelPrimitive from "@radix-ui/react-label";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ));
24 | Label.displayName = LabelPrimitive.Root.displayName;
25 |
26 | export { Label };
27 |
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ProgressPrimitive from "@radix-ui/react-progress";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ));
26 | Progress.displayName = ProgressPrimitive.Root.displayName;
27 |
28 | export { Progress };
29 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SelectPrimitive from "@radix-ui/react-select";
5 | import { Check, ChevronDown } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Select = SelectPrimitive.Root;
10 |
11 | const SelectGroup = SelectPrimitive.Group;
12 |
13 | const SelectValue = SelectPrimitive.Value;
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 |
27 | {children}
28 |
29 |
30 |
31 |
32 | ));
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
34 |
35 | const SelectContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, position = "popper", ...props }, ref) => (
39 |
40 |
50 |
57 | {children}
58 |
59 |
60 |
61 | ));
62 | SelectContent.displayName = SelectPrimitive.Content.displayName;
63 |
64 | const SelectLabel = React.forwardRef<
65 | React.ElementRef,
66 | React.ComponentPropsWithoutRef
67 | >(({ className, ...props }, ref) => (
68 |
73 | ));
74 | SelectLabel.displayName = SelectPrimitive.Label.displayName;
75 |
76 | const SelectItem = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, children, ...props }, ref) => (
80 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | {children}
95 |
96 | ));
97 | SelectItem.displayName = SelectPrimitive.Item.displayName;
98 |
99 | const SelectSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
110 |
111 | export {
112 | Select,
113 | SelectGroup,
114 | SelectValue,
115 | SelectTrigger,
116 | SelectContent,
117 | SelectLabel,
118 | SelectItem,
119 | SelectSeparator,
120 | };
121 |
--------------------------------------------------------------------------------
/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SliderPrimitive from "@radix-ui/react-slider";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ));
26 | Slider.displayName = SliderPrimitive.Root.displayName;
27 |
28 | export { Slider };
29 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SwitchPrimitives from "@radix-ui/react-switch";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ));
27 | Switch.displayName = SwitchPrimitives.Root.displayName;
28 |
29 | export { Switch };
30 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | }
21 | );
22 | Textarea.displayName = "Textarea";
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ToastPrimitives from "@radix-ui/react-toast";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 | import { X } from "lucide-react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const ToastProvider = ToastPrimitives.Provider;
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
24 |
25 | const toastVariants = cva(
26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-background",
31 | destructive:
32 | "destructive group border-destructive bg-destructive text-destructive-foreground",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | }
39 | );
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | );
53 | });
54 | Toast.displayName = ToastPrimitives.Root.displayName;
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ));
69 | ToastAction.displayName = ToastPrimitives.Action.displayName;
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ));
87 | ToastClose.displayName = ToastPrimitives.Close.displayName;
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ));
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName;
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ));
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName;
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef;
114 |
115 | type ToastActionElement = React.ReactElement;
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | };
128 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast";
11 | import { useToast } from "@/components/ui/use-toast";
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast();
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | );
31 | })}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
--------------------------------------------------------------------------------
/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react";
3 |
4 | import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
5 |
6 | const TOAST_LIMIT = 1;
7 | const TOAST_REMOVE_DELAY = 1000000;
8 |
9 | type ToasterToast = ToastProps & {
10 | id: string;
11 | title?: React.ReactNode;
12 | description?: React.ReactNode;
13 | action?: ToastActionElement;
14 | };
15 |
16 | const actionTypes = {
17 | ADD_TOAST: "ADD_TOAST",
18 | UPDATE_TOAST: "UPDATE_TOAST",
19 | DISMISS_TOAST: "DISMISS_TOAST",
20 | REMOVE_TOAST: "REMOVE_TOAST",
21 | } as const;
22 |
23 | let count = 0;
24 |
25 | function genId() {
26 | count = (count + 1) % Number.MAX_VALUE;
27 | return count.toString();
28 | }
29 |
30 | type ActionType = typeof actionTypes;
31 |
32 | type Action =
33 | | {
34 | type: ActionType["ADD_TOAST"];
35 | toast: ToasterToast;
36 | }
37 | | {
38 | type: ActionType["UPDATE_TOAST"];
39 | toast: Partial;
40 | }
41 | | {
42 | type: ActionType["DISMISS_TOAST"];
43 | toastId?: ToasterToast["id"];
44 | }
45 | | {
46 | type: ActionType["REMOVE_TOAST"];
47 | toastId?: ToasterToast["id"];
48 | };
49 |
50 | interface State {
51 | toasts: ToasterToast[];
52 | }
53 |
54 | const toastTimeouts = new Map>();
55 |
56 | const addToRemoveQueue = (toastId: string) => {
57 | if (toastTimeouts.has(toastId)) {
58 | return;
59 | }
60 |
61 | const timeout = setTimeout(() => {
62 | toastTimeouts.delete(toastId);
63 | dispatch({
64 | type: "REMOVE_TOAST",
65 | toastId: toastId,
66 | });
67 | }, TOAST_REMOVE_DELAY);
68 |
69 | toastTimeouts.set(toastId, timeout);
70 | };
71 |
72 | export const reducer = (state: State, action: Action): State => {
73 | switch (action.type) {
74 | case "ADD_TOAST":
75 | return {
76 | ...state,
77 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
78 | };
79 |
80 | case "UPDATE_TOAST":
81 | return {
82 | ...state,
83 | toasts: state.toasts.map((t) =>
84 | t.id === action.toast.id ? { ...t, ...action.toast } : t
85 | ),
86 | };
87 |
88 | case "DISMISS_TOAST": {
89 | const { toastId } = action;
90 |
91 | // ! Side effects ! - This could be extracted into a dismissToast() action,
92 | // but I'll keep it here for simplicity
93 | if (toastId) {
94 | addToRemoveQueue(toastId);
95 | } else {
96 | state.toasts.forEach((toast) => {
97 | addToRemoveQueue(toast.id);
98 | });
99 | }
100 |
101 | return {
102 | ...state,
103 | toasts: state.toasts.map((t) =>
104 | t.id === toastId || toastId === undefined
105 | ? {
106 | ...t,
107 | open: false,
108 | }
109 | : t
110 | ),
111 | };
112 | }
113 | case "REMOVE_TOAST":
114 | if (action.toastId === undefined) {
115 | return {
116 | ...state,
117 | toasts: [],
118 | };
119 | }
120 | return {
121 | ...state,
122 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
123 | };
124 | }
125 | };
126 |
127 | const listeners: Array<(state: State) => void> = [];
128 |
129 | let memoryState: State = { toasts: [] };
130 |
131 | function dispatch(action: Action) {
132 | memoryState = reducer(memoryState, action);
133 | listeners.forEach((listener) => {
134 | listener(memoryState);
135 | });
136 | }
137 |
138 | type Toast = Omit;
139 |
140 | function toast({ ...props }: Toast) {
141 | const id = genId();
142 |
143 | const update = (props: ToasterToast) =>
144 | dispatch({
145 | type: "UPDATE_TOAST",
146 | toast: { ...props, id },
147 | });
148 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
149 |
150 | dispatch({
151 | type: "ADD_TOAST",
152 | toast: {
153 | ...props,
154 | id,
155 | open: true,
156 | onOpenChange: (open) => {
157 | if (!open) dismiss();
158 | },
159 | },
160 | });
161 |
162 | return {
163 | id: id,
164 | dismiss,
165 | update,
166 | };
167 | }
168 |
169 | function useToast() {
170 | const [state, setState] = React.useState(memoryState);
171 |
172 | React.useEffect(() => {
173 | listeners.push(setState);
174 | return () => {
175 | const index = listeners.indexOf(setState);
176 | if (index > -1) {
177 | listeners.splice(index, 1);
178 | }
179 | };
180 | }, [state]);
181 |
182 | return {
183 | ...state,
184 | toast,
185 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
186 | };
187 | }
188 |
189 | export { useToast, toast };
190 |
--------------------------------------------------------------------------------
/constants/componentMappings.jsx:
--------------------------------------------------------------------------------
1 | import WebLoginForm from "@/components/Forms/WebLoginForm";
2 | import CreditCardForm from "@/components/Forms/CreditCardForm";
3 | import IdentificationDocumentForm from "@/components/Forms/IdentificationDocumentForm";
4 | import NoteForm from "@/components/Forms/NoteForm";
5 | import SocialMediaAccountForm from "@/components/Forms/SocialMediaAccountForm";
6 | import EmailAccountForm from "@/components/Forms/EmailAccountForm";
7 | import WiFiPasswordForm from "@/components/Forms/WiFiPasswordForm";
8 | import BankAccountForm from "@/components/Forms/BankAccountForm";
9 |
10 | import WebLoginCard from "@/components/Cards/WebLoginCard";
11 | import CreditCardCard from "@/components/Cards/CreditCardCard";
12 | import IdentificationDocumentCard from "@/components/Cards/IdentificationDocumentCard";
13 | import NoteCard from "@/components/Cards/NoteCard";
14 | import SocialMediaAccountCard from "@/components/Cards/SocialMediaAccountCard";
15 | import EmailAccountCard from "@/components/Cards/EmailAccountCard";
16 | import WiFiPasswordCard from "@/components/Cards/WiFiPasswordCard";
17 | import BankAccountCard from "@/components/Cards/BankAccountCard";
18 |
19 | import {
20 | Globe,
21 | CreditCard,
22 | UserCircle2,
23 | FileText,
24 | AtSign,
25 | Mail,
26 | Wifi,
27 | Landmark,
28 | } from "lucide-react";
29 |
30 | export const formComponents = {
31 | webLogin: ,
32 | creditCard: ,
33 | identificationDocument: ,
34 | note: ,
35 | socialMediaAccount: ,
36 | emailAccount: ,
37 | wiFiPassword: ,
38 | bankAccount: ,
39 | };
40 |
41 | export const cardComponents = {
42 | webLogin: WebLoginCard,
43 | creditCard: CreditCardCard,
44 | identificationDocument: IdentificationDocumentCard,
45 | note: NoteCard,
46 | socialMediaAccount: SocialMediaAccountCard,
47 | emailAccount: EmailAccountCard,
48 | wiFiPassword: WiFiPasswordCard,
49 | bankAccount: BankAccountCard,
50 | };
51 |
52 | export const passwordIcons = {
53 | webLogin: Globe,
54 | creditCard: CreditCard,
55 | identificationDocument: UserCircle2,
56 | note: FileText,
57 | socialMediaAccount: AtSign,
58 | emailAccount: Mail,
59 | wiFiPassword: Wifi,
60 | bankAccount: Landmark,
61 | };
62 |
63 | export const passwordTypeStates = {
64 | webLogin: {
65 | passwordType: "webLogin",
66 | passwordTitle: "",
67 | email: "",
68 | password: "",
69 | URL: "",
70 | },
71 | creditCard: {
72 | passwordType: "creditCard",
73 | passwordTitle: "",
74 | creditCardNumber: "",
75 | cardHolderName: "",
76 | expirationDate: "",
77 | securityCode: "",
78 | },
79 | identificationDocument: {
80 | passwordType: "identificationDocument",
81 | passwordTitle: "",
82 | identityNumber: "",
83 | fullName: "",
84 | birthDate: "",
85 | seriesNumber: "",
86 | },
87 | note: {
88 | passwordType: "note",
89 | passwordTitle: "",
90 | note: "",
91 | },
92 | socialMediaAccount: {
93 | passwordType: "socialMediaAccount",
94 | passwordTitle: "",
95 | username: "",
96 | password: "",
97 | },
98 | emailAccount: {
99 | passwordType: "emailAccount",
100 | passwordTitle: "",
101 | email: "",
102 | password: "",
103 | },
104 | wiFiPassword: {
105 | passwordType: "wiFiPassword",
106 | passwordTitle: "",
107 | wifiName: "",
108 | wifiPassword: "",
109 | },
110 | bankAccount: {
111 | passwordType: "bankAccount",
112 | passwordTitle: "",
113 | iban: "",
114 | fullName: "",
115 | },
116 | };
117 |
--------------------------------------------------------------------------------
/i18n.js:
--------------------------------------------------------------------------------
1 | import i18n from "i18next";
2 |
3 | import { initReactI18next } from "react-i18next";
4 |
5 | import TranslationEN from "@/locales/en/translation.json";
6 | import TranslationTR from "@/locales/tr/translation.json";
7 |
8 | const resources = {
9 | en: {
10 | translation: TranslationEN,
11 | },
12 | tr: {
13 | translation: TranslationTR,
14 | },
15 | };
16 |
17 | i18n.use(initReactI18next).init({
18 | resources,
19 | lng: "auto",
20 | fallbackLng: "en",
21 | interpolation: {
22 | escapeValue: false,
23 | },
24 | });
25 |
26 | export default i18n;
27 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["./*"]
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "authentication": {
3 | "form": {
4 | "heading": "Secure Access to Your Passwords",
5 | "subheading": "Safeguarding your passwords has never been easier. Just enter your email and master password to access your encrypted vault. Your existing encrypted file will be unlocked, or a new one will be created if it doesn't exist.",
6 | "email_label": "Email",
7 | "email_placeholder": "Enter your email",
8 | "password_label": "Password",
9 | "password_placeholder": "Enter your password",
10 | "continue_button": "Continue",
11 | "error_title": "Oops! Something went wrong.",
12 | "error_description": "An error occurred while attempting to access your password vault or create a new encrypted file."
13 | }
14 | },
15 | "dashboard": {
16 | "sidebar": {
17 | "passwords": "Passwords",
18 | "passwords_children": {
19 | "passwords": "Passwords"
20 | },
21 | "categories": "Categories",
22 | "categories_children": {
23 | "webLogins": "Web Logins",
24 | "creditCards": "Credit Cards",
25 | "identificationDocuments": "Identity Documents",
26 | "notes": "Notes",
27 | "socialMediaAccounts": "Social Media Accounts",
28 | "emailAccounts": "Email Accounts",
29 | "wiFiPasswords": "WiFi Passwords",
30 | "bankAccounts": "Bank Accounts"
31 | }
32 | },
33 | "header": {
34 | "password_types": {
35 | "passwords": "All Passwords",
36 | "webLogin": "Web Logins",
37 | "creditCard": "Credit Cards",
38 | "identificationDocument": "Identity Documents",
39 | "note": "Notes",
40 | "socialMediaAccount": "Social Media Accounts",
41 | "emailAccount": "Email Accounts",
42 | "wiFiPassword": "WiFi Passwords",
43 | "bankAccount": "Bank Accounts"
44 | },
45 | "description": "Safely manage and access your passwords."
46 | },
47 | "search_bar": {
48 | "search_placeholder": "Search"
49 | },
50 | "passwords": {
51 | "edit_button": "Edit"
52 | },
53 | "password_dialog": {
54 | "add_new_password_button": "Add New Password",
55 | "dialog_title_edit": "Edit Password",
56 | "dialog_title_add": "Add New Password",
57 | "dialog_description_edit": "Enter the necessary information to edit this password and save.",
58 | "dialog_description_add": "Enter the necessary information to create a new password and save.",
59 | "dialog_cancel_button": "Cancel",
60 | "dialog_back_button": "Back",
61 | "dialog_save_button": "Save",
62 | "dialog_next_button": "Next"
63 | },
64 | "password_form": {
65 | "password_type_label": "Password Type",
66 | "password_type_placeholder": "Select password type",
67 | "password_title_label": "Title",
68 | "password_title_placeholder": "Enter title",
69 | "password_types": {
70 | "webLogin": "Web Login",
71 | "creditCard": "Credit Card",
72 | "identificationDocument": "Identity Document",
73 | "note": "Note",
74 | "socialMediaAccount": "Social Media Account",
75 | "emailAccount": "Email Account",
76 | "wiFiPassword": "WiFi Password",
77 | "bankAccount": "Bank Account"
78 | }
79 | },
80 | "bank_account_form": {
81 | "iban_label": "IBAN",
82 | "iban_placeholder": "GB",
83 | "full_name_label": "Full Name",
84 | "full_name_placeholder": "Enter full name"
85 | },
86 | "credit_card_form": {
87 | "credit_card_number_label": "Card Number",
88 | "credit_card_number_placeholder": "•••• •••• •••• ••••",
89 | "card_holder_name_label": "Name on Card",
90 | "card_holder_name_placeholder": "Enter the name on the card",
91 | "expiration_date_label": "Expiration Date",
92 | "expiration_date_placeholder": "MM / YY ",
93 | "security_code_label": "Security Code",
94 | "security_code_placeholder": "CVC / CVV"
95 | },
96 | "email_account_form": {
97 | "email_label": "Email",
98 | "email_placeholder": "Enter your email",
99 | "password_label": "Password",
100 | "password_placeholder": "Enter your password"
101 | },
102 | "identification_document_form": {
103 | "identity_number_label": "ID Number",
104 | "identity_number_placeholder": "Enter your ID number",
105 | "full_name_label": "Full Name",
106 | "full_name_placeholder": "Enter your full name",
107 | "birth_date_label": "Birth Date",
108 | "birth_date_placeholder": "DD / MM / YYYY",
109 | "series_number_label": "Serial Number",
110 | "series_number_placeholder": "Enter the serial number"
111 | },
112 | "note_form": {
113 | "note_label": "Note",
114 | "note_placeholder": "Enter your note"
115 | },
116 | "social_media_account_form": {
117 | "username_label": "Username",
118 | "username_placeholder": "Enter your username",
119 | "password_label": "Password",
120 | "password_placeholder": "Enter your password"
121 | },
122 | "web_login_form": {
123 | "email_label": "Email",
124 | "email_placeholder": "Enter your email",
125 | "password_label": "Password",
126 | "password_placeholder": "Enter your password",
127 | "url_label": "URL",
128 | "url_placeholder": "Enter the URL"
129 | },
130 | "wifi_password_form": {
131 | "wifi_name_label": "WiFi Name",
132 | "wifi_name_placeholder": "Enter the WiFi name",
133 | "wifi_password_label": "WiFi Password",
134 | "wifi_password_placeholder": "Enter the WiFi password"
135 | },
136 | "delete_password_dialog": {
137 | "delete_button": "Delete",
138 | "dialog_title_delete": "Delete Password",
139 | "dialog_description_delete": "Are you sure you want to delete this password permanently?",
140 | "dialog_cancel_button": "Cancel",
141 | "dialog_delete_button": "Delete"
142 | },
143 | "password_generator": {
144 | "error_title": "Oops! Something went wrong.",
145 | "error_description": "You need to select at least one criterion to generate an automatic password.",
146 | "button": "Generate Automatically",
147 | "length": "Length",
148 | "lowercase": "Lowercase Letters",
149 | "uppercase": "Uppercase Letters",
150 | "digit": "Digits",
151 | "special_character": "Special Characters"
152 | },
153 | "password_strength": {
154 | "is_length_sufficient": "It's recommended to have passwords with a minimum length of 8 characters.",
155 | "has_lowercase": "Using lowercase letters in passwords is recommended.",
156 | "has_uppercase": "Using uppercase letters in passwords is recommended.",
157 | "has_digit": "Including digits in passwords is recommended.",
158 | "has_special_character": "Using special characters in passwords is recommended."
159 | },
160 | "empty_state": {
161 | "title": "No passwords found",
162 | "description": "No passwords in your vault or your search didn't match."
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/locales/tr/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "authentication": {
3 | "form": {
4 | "heading": "Şifrelerinize Güvenli Erişim",
5 | "subheading": "Şifrelerinizi korumanın artık daha kolay bir yolu var. Sadece e-posta adresinizi ve ana şifrenizi girerek şifrelenmiş kasanıza erişebilirsiniz. Varolan şifrelenmiş dosyanız açılacak ya da yoksa yeni bir dosya oluşturulacaktır.",
6 | "email_label": "E-posta",
7 | "email_placeholder": "E-posta adresinizi girin",
8 | "password_label": "Şifre",
9 | "password_placeholder": "Şifrenizi girin",
10 | "continue_button": "Devam Et",
11 | "error_title": "Üzgünüz! Bir şeyler ters gitti.",
12 | "error_description": "Şifre depo erişimine veya yeni şifrelenmiş bir dosya oluşturmaya çalışırken bir hata oluştu."
13 | }
14 | },
15 | "dashboard": {
16 | "sidebar": {
17 | "passwords": "Passwords",
18 | "passwords_children": {
19 | "passwords": "Şifreler"
20 | },
21 | "categories": "Kategoriler",
22 | "categories_children": {
23 | "webLogins": "Web Girişleri",
24 | "creditCards": "Kredi Kartları",
25 | "identificationDocuments": "Kimlik Belgeleri",
26 | "notes": "Notlar",
27 | "socialMediaAccounts": "Sosyal Medya Hesapları",
28 | "emailAccounts": "E-posta Hesapları",
29 | "wiFiPasswords": "WiFi Şifreleri",
30 | "bankAccounts": "Banka Hesapları"
31 | }
32 | },
33 | "header": {
34 | "password_types": {
35 | "passwords": "Tüm Şifreler",
36 | "webLogin": "Web Girişleri",
37 | "creditCard": "Kredi Kartları",
38 | "identificationDocument": "Kimlik Belgeleri",
39 | "note": "Notlar",
40 | "socialMediaAccount": "Sosyal Medya Hesapları",
41 | "emailAccount": "E-posta Hesapları",
42 | "wiFiPassword": "WiFi Şifreleri",
43 | "bankAccount": "Banka Hesapları"
44 | },
45 | "description": "Şifrelerinizi güvenli bir şekilde yönetin ve erişim sağlayın."
46 | },
47 | "search_bar": {
48 | "search_placeholder": "Ara"
49 | },
50 | "passwords": {
51 | "edit_button": "Düzenle"
52 | },
53 | "password_dialog": {
54 | "add_new_password_button": "Yeni Şifre Ekle",
55 | "dialog_title_edit": "Şifreyi Düzenle",
56 | "dialog_title_add": "Yeni Şifre Ekle",
57 | "dialog_description_edit": "Bu şifreyi düzenlemek için gerekli bilgileri girin ve kaydedin.",
58 | "dialog_description_add": "Yeni bir şifre oluşturmak için gerekli bilgileri girin ve kaydedin.",
59 | "dialog_cancel_button": "İptal",
60 | "dialog_back_button": "Geri",
61 | "dialog_save_button": "Kaydet",
62 | "dialog_next_button": "İleri"
63 | },
64 | "password_form": {
65 | "password_type_label": "Şifre Türü",
66 | "password_type_placeholder": "Şifre türü seçiniz",
67 | "password_title_label": "Başlık",
68 | "password_title_placeholder": "Başlık giriniz",
69 | "password_types": {
70 | "webLogin": "Web Girişi",
71 | "creditCard": "Kredi Kartı",
72 | "identificationDocument": "Kimlik Belgesi",
73 | "note": "Not",
74 | "socialMediaAccount": "Sosyal Medya Hesabı",
75 | "emailAccount": "E-posta Hesabı",
76 | "wiFiPassword": "WiFi Şifresi",
77 | "bankAccount": "Banka Hesabı"
78 | }
79 | },
80 | "bank_account_form": {
81 | "iban_label": "IBAN",
82 | "iban_placeholder": "TR",
83 | "full_name_label": "Ad Soyad",
84 | "full_name_placeholder": "Ad soyad giriniz"
85 | },
86 | "credit_card_form": {
87 | "credit_card_number_label": "Kart Numarası",
88 | "credit_card_number_placeholder": "•••• •••• •••• ••••",
89 | "card_holder_name_label": "Kart Üzerindeki İsim",
90 | "card_holder_name_placeholder": "Kart üzerindeki ismi giriniz",
91 | "expiration_date_label": "Son Kullanma Tarihi",
92 | "expiration_date_placeholder": "AA / YY",
93 | "security_code_label": "Güvenlik Kodu",
94 | "security_code_placeholder": "CVC / CVV"
95 | },
96 | "email_account_form": {
97 | "email_label": "E-posta",
98 | "email_placeholder": "E-posta giriniz",
99 | "password_label": "Şifre",
100 | "password_placeholder": "Şifre giriniz"
101 | },
102 | "identification_document_form": {
103 | "identity_number_label": "Kimlik Numarası",
104 | "identity_number_placeholder": "Kimlik numarası giriniz",
105 | "full_name_label": "Ad Soyad",
106 | "full_name_placeholder": "Ad soyad giriniz",
107 | "birth_date_label": "Doğum Tarihi",
108 | "birth_date_placeholder": "GG / AA / YYYY",
109 | "series_number_label": "Seri Numarası",
110 | "series_number_placeholder": "Seri numarası giriniz"
111 | },
112 | "note_form": {
113 | "note_label": "Not",
114 | "note_placeholder": "Not giriniz"
115 | },
116 | "social_media_account_form": {
117 | "username_label": "Kullanıcı Adı",
118 | "username_placeholder": "Kullanıcı adı giriniz",
119 | "password_label": "Şifre",
120 | "password_placeholder": "Şifre giriniz"
121 | },
122 | "web_login_form": {
123 | "email_label": "E-posta",
124 | "email_placeholder": "E-posta giriniz",
125 | "password_label": "Şifre",
126 | "password_placeholder": "Şifre giriniz",
127 | "url_label": "URL",
128 | "url_placeholder": "URL giriniz"
129 | },
130 | "wifi_password_form": {
131 | "wifi_name_label": "WiFi İsmi",
132 | "wifi_name_placeholder": "WiFi ismi giriniz",
133 | "wifi_password_label": "WiFi Şifresi",
134 | "wifi_password_placeholder": "WiFi şifresi giriniz"
135 | },
136 | "delete_password_dialog": {
137 | "delete_button": "Sil",
138 | "dialog_title_delete": "Şifreyi Sil",
139 | "dialog_description_delete": "Bu şifreyi kalıcı olarak silmek istediğinizden emin misiniz?",
140 | "dialog_cancel_button": "İptal",
141 | "dialog_delete_button": "Sil"
142 | },
143 | "password_generator": {
144 | "error_title": "Üzgünüz! Bir şeyler ters gitti.",
145 | "error_description": "Otomatik bir şifre oluşturmak için en az bir kriter seçmelisiniz.",
146 | "button": "Otomatik Oluştur",
147 | "length": "Uzunluk",
148 | "lowercase": "Küçük Harfler",
149 | "uppercase": "Büyük Harfler",
150 | "digit": "Rakamlar",
151 | "special_character": "Özel Karakterler"
152 | },
153 | "password_strength": {
154 | "is_length_sufficient": "Şifrelerin en az 8 karakter uzunluğunda olması önerilir.",
155 | "has_lowercase": "Şifrelerde küçük harf kullanmanız önerilir.",
156 | "has_uppercase": "Şifrelerde büyük harf kullanmanız önerilir.",
157 | "has_digit": "Şifrelerde rakam kullanmanız önerilir.",
158 | "has_special_character": "Şifrelerde özel karakter kullanmanız önerilir."
159 | },
160 | "empty_state": {
161 | "title": "Şifre Bulunamadı",
162 | "description": "Kasanızda şifre bulunmuyor veya aramanız eşleşmedi."
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | output: "export",
4 | };
5 |
6 | module.exports = nextConfig;
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "passwords",
3 | "version": "0.1.0",
4 | "description": "Easily secure and manage passwords with a desktop app powered by Tauri and Next.js.",
5 | "homepage": "https://github.com/pekkiriscim/passwords",
6 | "license": "MIT",
7 | "author": {
8 | "name": "Mustafa Pekkirişci"
9 | },
10 | "scripts": {
11 | "dev": "next dev",
12 | "build": "next build",
13 | "start": "next start",
14 | "lint": "next lint",
15 | "tauri": "tauri",
16 | "prettier": "npx prettier --write ."
17 | },
18 | "dependencies": {
19 | "@radix-ui/react-accordion": "^1.1.2",
20 | "@radix-ui/react-dialog": "^1.0.4",
21 | "@radix-ui/react-label": "^2.0.2",
22 | "@radix-ui/react-progress": "^1.0.3",
23 | "@radix-ui/react-select": "^1.2.2",
24 | "@radix-ui/react-slider": "^1.1.2",
25 | "@radix-ui/react-slot": "^1.0.2",
26 | "@radix-ui/react-switch": "^1.0.3",
27 | "@radix-ui/react-toast": "^1.1.4",
28 | "@radix-ui/react-tooltip": "^1.0.6",
29 | "@tauri-apps/api": "^1.4.0",
30 | "autoprefixer": "10.4.14",
31 | "class-variance-authority": "^0.6.0",
32 | "clsx": "^1.2.1",
33 | "crypto-js": "^4.1.1",
34 | "eslint": "8.41.0",
35 | "eslint-config-next": "13.4.4",
36 | "i18next": "^23.4.1",
37 | "lucide-react": "^0.233.0",
38 | "next": "13.4.4",
39 | "postcss": "8.4.24",
40 | "react": "18.2.0",
41 | "react-dom": "18.2.0",
42 | "react-i18next": "^13.0.2",
43 | "tailwind-merge": "^1.13.0",
44 | "tailwindcss": "3.3.2",
45 | "tailwindcss-animate": "^1.0.5"
46 | },
47 | "devDependencies": {
48 | "@tauri-apps/cli": "^1.3.1",
49 | "prettier": "2.8.8"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/passwords.config.js:
--------------------------------------------------------------------------------
1 | import { BaseDirectory } from "@tauri-apps/api/fs";
2 |
3 | export const file_extension = "passwords";
4 |
5 | export const file_path = BaseDirectory.AppData;
6 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/screenshots/Authentication.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/screenshots/Authentication.png
--------------------------------------------------------------------------------
/screenshots/Dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/screenshots/Dashboard.png
--------------------------------------------------------------------------------
/screenshots/DeletePasswordDialog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/screenshots/DeletePasswordDialog.png
--------------------------------------------------------------------------------
/screenshots/PasswordDialog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/screenshots/PasswordDialog.png
--------------------------------------------------------------------------------
/screenshots/PasswordGenerator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/screenshots/PasswordGenerator.png
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "passwords"
3 | version = "0.1.0"
4 | description = "Easily secure and manage passwords with a desktop app powered by Tauri and Next.js."
5 | authors = ["Mustafa Pekkirişci"]
6 | license = "MIT"
7 | repository = "https://github.com/pekkiriscim/passwords"
8 | default-run = "passwords"
9 | edition = "2021"
10 | rust-version = "1.60"
11 |
12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13 |
14 | [build-dependencies]
15 | tauri-build = { version = "1.3.0", features = [] }
16 |
17 | [dependencies]
18 | serde_json = "1.0"
19 | serde = { version = "1.0", features = ["derive"] }
20 | tauri = { version = "1.3.0", features = ["fs-create-dir", "fs-exists", "fs-read-file", "fs-write-file"] }
21 |
22 | [features]
23 | # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
24 | # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
25 | # DO NOT REMOVE!!
26 | custom-protocol = [ "tauri/custom-protocol" ]
27 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pekkiriscim/passwords/42f1f792b6d7c7ce621902719a1d63fdd57b0234/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!!
2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3 |
4 | fn main() {
5 | tauri::Builder::default()
6 | .run(tauri::generate_context!())
7 | .expect("error while running tauri application");
8 | }
9 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json",
3 | "build": {
4 | "beforeBuildCommand": "npm run build",
5 | "beforeDevCommand": "npm run dev",
6 | "devPath": "http://localhost:3000",
7 | "distDir": "../out"
8 | },
9 | "package": {
10 | "productName": "Passwords",
11 | "version": "0.1.0"
12 | },
13 | "tauri": {
14 | "allowlist": {
15 | "all": false,
16 | "fs": {
17 | "all": false,
18 | "copyFile": false,
19 | "createDir": true,
20 | "exists": true,
21 | "readDir": false,
22 | "readFile": true,
23 | "removeDir": false,
24 | "removeFile": false,
25 | "renameFile": false,
26 | "scope": ["$APPDATA/*", "$APPDATA"],
27 | "writeFile": true
28 | }
29 | },
30 | "bundle": {
31 | "active": true,
32 | "category": "DeveloperTool",
33 | "copyright": "",
34 | "deb": {
35 | "depends": []
36 | },
37 | "externalBin": [],
38 | "icon": [
39 | "icons/32x32.png",
40 | "icons/128x128.png",
41 | "icons/128x128@2x.png",
42 | "icons/icon.icns",
43 | "icons/icon.ico"
44 | ],
45 | "identifier": "com.pekkiriscim.passwords",
46 | "longDescription": "Passwords is an open-source desktop app that simplifies password management, offering a secure and user-friendly experience. Built with Tauri and Next.js.",
47 | "macOS": {
48 | "entitlements": null,
49 | "exceptionDomain": "",
50 | "frameworks": [],
51 | "providerShortName": null,
52 | "signingIdentity": null
53 | },
54 | "resources": [],
55 | "shortDescription": "Easily secure and manage passwords with a desktop app powered by Tauri and Next.js.",
56 | "targets": "all",
57 | "windows": {
58 | "certificateThumbprint": null,
59 | "digestAlgorithm": "sha256",
60 | "timestampUrl": ""
61 | }
62 | },
63 | "security": {
64 | "csp": null
65 | },
66 | "updater": {
67 | "active": false
68 | },
69 | "windows": [
70 | {
71 | "fullscreen": false,
72 | "height": 800,
73 | "minHeight": 800,
74 | "resizable": true,
75 | "title": "Passwords",
76 | "width": 1024,
77 | "minWidth": 640,
78 | "center": true
79 | }
80 | ]
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 47.4% 11.2%;
9 |
10 | --muted: 210 40% 96.1%;
11 | --muted-foreground: 215.4 16.3% 46.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 47.4% 11.2%;
15 |
16 | --card: 0 0% 100%;
17 | --card-foreground: 222.2 47.4% 11.2%;
18 |
19 | --border: 214.3 31.8% 91.4%;
20 | --input: 214.3 31.8% 91.4%;
21 |
22 | --primary: 222.2 47.4% 11.2%;
23 | --primary-foreground: 210 40% 98%;
24 |
25 | --secondary: 210 40% 96.1%;
26 | --secondary-foreground: 222.2 47.4% 11.2%;
27 |
28 | --accent: 210 40% 96.1%;
29 | --accent-foreground: 222.2 47.4% 11.2%;
30 |
31 | --destructive: 0 100% 50%;
32 | --destructive-foreground: 210 40% 98%;
33 |
34 | --ring: 215 20.2% 65.1%;
35 |
36 | --radius: 0.5rem;
37 | }
38 |
39 | @media (prefers-color-scheme: dark) {
40 | :root {
41 | --background: 224 71% 4%;
42 | --foreground: 213 31% 91%;
43 |
44 | --muted: 223 47% 11%;
45 | --muted-foreground: 215.4 16.3% 56.9%;
46 |
47 | --popover: 224 71% 4%;
48 | --popover-foreground: 215 20.2% 65.1%;
49 |
50 | --card: 224 71% 4%;
51 | --card-foreground: 213 31% 91%;
52 |
53 | --border: 216 34% 17%;
54 | --input: 216 34% 17%;
55 |
56 | --primary: 210 40% 98%;
57 | --primary-foreground: 222.2 47.4% 1.2%;
58 |
59 | --secondary: 222.2 47.4% 11.2%;
60 | --secondary-foreground: 210 40% 98%;
61 |
62 | --accent: 216 34% 17%;
63 | --accent-foreground: 210 40% 98%;
64 |
65 | --destructive: 0 63% 31%;
66 | --destructive-foreground: 210 40% 98%;
67 |
68 | --ring: 216 34% 17%;
69 |
70 | --radius: 0.5rem;
71 | }
72 | }
73 | }
74 |
75 | @layer base {
76 | * {
77 | @apply border-border;
78 | }
79 |
80 | html,
81 | body {
82 | @apply bg-background text-foreground w-full h-full select-none cursor-default antialiased;
83 | font-feature-settings: "rlig" 1, "calt" 1;
84 | text-rendering: optimizeLegibility;
85 | }
86 |
87 | html::-webkit-scrollbar,
88 | .hide-scrollbar::-webkit-scrollbar {
89 | display: none;
90 | }
91 |
92 | html,
93 | .hide-scrollbar {
94 | -ms-overflow-style: none;
95 | scrollbar-width: none;
96 | }
97 | }
98 |
99 | @layer components {
100 | .card-wrapper > * {
101 | @apply overflow-auto whitespace-nowrap hide-scrollbar;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
5 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./data/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | container: {
11 | center: true,
12 | padding: "2rem",
13 | screens: {
14 | "2xl": "1400px",
15 | },
16 | },
17 | fontFamily: {
18 | sans: [
19 | "ui-sans-serif",
20 | "system-ui",
21 | "-apple-system",
22 | "BlinkMacSystemFont",
23 | "Segoe UI",
24 | "Roboto",
25 | "Helvetica Neue",
26 | "Arial",
27 | "Noto Sans",
28 | "sans-serif",
29 | "Apple Color Emoji",
30 | "Segoe UI Emoji",
31 | "Segoe UI Symbol",
32 | "Noto Color Emoji",
33 | ],
34 | },
35 | extend: {
36 | colors: {
37 | border: "hsl(var(--border))",
38 | input: "hsl(var(--input))",
39 | ring: "hsl(var(--ring))",
40 | background: "hsl(var(--background))",
41 | foreground: "hsl(var(--foreground))",
42 | primary: {
43 | DEFAULT: "hsl(var(--primary))",
44 | foreground: "hsl(var(--primary-foreground))",
45 | },
46 | secondary: {
47 | DEFAULT: "hsl(var(--secondary))",
48 | foreground: "hsl(var(--secondary-foreground))",
49 | },
50 | destructive: {
51 | DEFAULT: "hsl(var(--destructive))",
52 | foreground: "hsl(var(--destructive-foreground))",
53 | },
54 | muted: {
55 | DEFAULT: "hsl(var(--muted))",
56 | foreground: "hsl(var(--muted-foreground))",
57 | },
58 | accent: {
59 | DEFAULT: "hsl(var(--accent))",
60 | foreground: "hsl(var(--accent-foreground))",
61 | },
62 | popover: {
63 | DEFAULT: "hsl(var(--popover))",
64 | foreground: "hsl(var(--popover-foreground))",
65 | },
66 | card: {
67 | DEFAULT: "hsl(var(--card))",
68 | foreground: "hsl(var(--card-foreground))",
69 | },
70 | },
71 | borderRadius: {
72 | lg: `var(--radius)`,
73 | md: `calc(var(--radius) - 2px)`,
74 | sm: "calc(var(--radius) - 4px)",
75 | xsm: "calc(var(--radius) - 5px)",
76 | },
77 | keyframes: {
78 | "accordion-down": {
79 | from: { height: 0 },
80 | to: { height: "var(--radix-accordion-content-height)" },
81 | },
82 | "accordion-up": {
83 | from: { height: "var(--radix-accordion-content-height)" },
84 | to: { height: 0 },
85 | },
86 | },
87 | animation: {
88 | "accordion-down": "accordion-down 0.2s ease-out",
89 | "accordion-up": "accordion-up 0.2s ease-out",
90 | },
91 | },
92 | },
93 | plugins: [require("tailwindcss-animate")],
94 | };
95 |
--------------------------------------------------------------------------------
/utils/addNewPassword.js:
--------------------------------------------------------------------------------
1 | import { savePasswords } from "@/utils/savePasswords";
2 |
3 | export async function addNewPassword(passwords, newPassword, auth) {
4 | try {
5 | const currentTimestamp = Date.now();
6 |
7 | const newPasswords = [
8 | ...passwords,
9 | { ...newPassword, passwordId: currentTimestamp },
10 | ];
11 |
12 | const newPasswordsState = await savePasswords(
13 | newPasswords,
14 | auth.email,
15 | auth.password
16 | );
17 |
18 | if (newPasswordsState) {
19 | return newPasswordsState;
20 | }
21 | } catch (error) {
22 | console.log(error);
23 |
24 | return null;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/utils/calculateStrength.js:
--------------------------------------------------------------------------------
1 | export const calculateStrength = (password) => {
2 | let strength = {
3 | value: 0,
4 | isLengthSufficient: false,
5 | hasUppercase: false,
6 | hasLowercase: false,
7 | hasDigit: false,
8 | hasSpecialCharacter: false,
9 | };
10 |
11 | if (password.length >= 8) {
12 | strength.value += 1;
13 | strength.isLengthSufficient = true;
14 | }
15 |
16 | if (/[A-Z]/.test(password)) {
17 | strength.value += 1;
18 | strength.hasUppercase = true;
19 | }
20 |
21 | if (/[a-z]/.test(password)) {
22 | strength.value += 1;
23 | strength.hasLowercase = true;
24 | }
25 |
26 | if (/\d/.test(password)) {
27 | strength.value += 1;
28 | strength.hasDigit = true;
29 | }
30 |
31 | if (/[!@#$%^&*()\-=_+[\]{}|:;"'<>,.?/~`]/.test(password)) {
32 | strength.value += 1;
33 | strength.hasSpecialCharacter = true;
34 | }
35 |
36 | const strengthPercentage = (strength.value / 5) * 100;
37 | strength.value = strengthPercentage > 100 ? 100 : strengthPercentage;
38 |
39 | return strength;
40 | };
41 |
--------------------------------------------------------------------------------
/utils/generatePassword.js:
--------------------------------------------------------------------------------
1 | export const generatePassword = (generateSettings) => {
2 | const {
3 | length,
4 | includeUpperCase,
5 | includeLowerCase,
6 | includeDigits,
7 | includeSpecialChars,
8 | } = generateSettings;
9 |
10 | if (
11 | !includeUpperCase &&
12 | !includeLowerCase &&
13 | !includeDigits &&
14 | !includeSpecialChars
15 | ) {
16 | return null;
17 | }
18 |
19 | let charset = "";
20 |
21 | if (includeUpperCase) {
22 | charset += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
23 | }
24 |
25 | if (includeLowerCase) {
26 | charset += "abcdefghijklmnopqrstuvwxyz";
27 | }
28 |
29 | if (includeDigits) {
30 | charset += "0123456789";
31 | }
32 |
33 | if (includeSpecialChars) {
34 | charset += "!@#$%^&*()-_=+[{]}\\|;:',<.>/?";
35 | }
36 |
37 | let password = "";
38 | const charsetLength = charset.length;
39 |
40 | for (let i = 0; i < length; i++) {
41 | const randomIndex = Math.floor(Math.random() * charsetLength);
42 |
43 | password += charset[randomIndex];
44 | }
45 |
46 | return password;
47 | };
48 |
--------------------------------------------------------------------------------
/utils/handleAuthentication.js:
--------------------------------------------------------------------------------
1 | import {
2 | exists,
3 | writeTextFile,
4 | readTextFile,
5 | createDir,
6 | } from "@tauri-apps/api/fs";
7 |
8 | import SHA256 from "crypto-js/sha256";
9 | import AES from "crypto-js/aes";
10 | import encUTF8 from "crypto-js/enc-utf8";
11 |
12 | import { file_path, file_extension } from "@/passwords.config";
13 |
14 | export const handleAuthentication = async (email, password) => {
15 | try {
16 | const fileName = SHA256(email + password).toString();
17 | const filePath = file_path;
18 | const fileExtension = file_extension;
19 |
20 | const fileNameWithExtension = `${fileName}.${fileExtension}`;
21 |
22 | const isFileExists = await exists(fileNameWithExtension, {
23 | dir: filePath,
24 | });
25 |
26 | if (isFileExists) {
27 | const fileContent = await readTextFile(fileNameWithExtension, {
28 | dir: filePath,
29 | });
30 |
31 | const decryptedContent = AES.decrypt(
32 | fileContent,
33 | email + password
34 | ).toString(encUTF8);
35 |
36 | const parsedContent = JSON.parse(decryptedContent);
37 |
38 | return parsedContent;
39 | } else {
40 | await createDir("", { dir: filePath, recursive: true });
41 |
42 | const encryptedContent = AES.encrypt(
43 | JSON.stringify([]),
44 | email + password
45 | ).toString();
46 |
47 | await writeTextFile(fileNameWithExtension, encryptedContent, {
48 | dir: filePath,
49 | });
50 |
51 | const fileContent = await readTextFile(fileNameWithExtension, {
52 | dir: filePath,
53 | });
54 |
55 | const decryptedContent = AES.decrypt(
56 | fileContent,
57 | email + password
58 | ).toString(encUTF8);
59 |
60 | const parsedContent = JSON.parse(decryptedContent);
61 |
62 | return parsedContent;
63 | }
64 | } catch (error) {
65 | console.log(error);
66 |
67 | return null;
68 | }
69 | };
70 |
--------------------------------------------------------------------------------
/utils/savePasswords.js:
--------------------------------------------------------------------------------
1 | import { writeTextFile, readTextFile } from "@tauri-apps/api/fs";
2 |
3 | import SHA256 from "crypto-js/sha256";
4 | import AES from "crypto-js/aes";
5 | import encUTF8 from "crypto-js/enc-utf8";
6 |
7 | import { file_path, file_extension } from "@/passwords.config";
8 |
9 | export const savePasswords = async (newPasswords, email, password) => {
10 | try {
11 | const fileName = SHA256(email + password).toString();
12 | const filePath = file_path;
13 | const fileExtension = file_extension;
14 |
15 | const fileNameWithExtension = `${fileName}.${fileExtension}`;
16 |
17 | const encryptedContent = AES.encrypt(
18 | JSON.stringify(newPasswords),
19 | email + password
20 | ).toString();
21 |
22 | await writeTextFile(fileNameWithExtension, encryptedContent, {
23 | dir: filePath,
24 | });
25 |
26 | const fileContent = await readTextFile(fileNameWithExtension, {
27 | dir: filePath,
28 | });
29 |
30 | const decryptedContent = AES.decrypt(
31 | fileContent,
32 | email + password
33 | ).toString(encUTF8);
34 |
35 | const parsedContent = JSON.parse(decryptedContent);
36 |
37 | return parsedContent;
38 | } catch (error) {
39 | console.log(error);
40 |
41 | return null;
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/utils/updatePassword.js:
--------------------------------------------------------------------------------
1 | import { savePasswords } from "@/utils/savePasswords";
2 |
3 | export async function updatePassword(passwords, password, newPassword, auth) {
4 | try {
5 | const updatedPasswords = [...passwords];
6 |
7 | const index = updatedPasswords.findIndex(
8 | (obj) => obj.passwordId === password.passwordId
9 | );
10 |
11 | if (index !== -1) {
12 | updatedPasswords[index] = newPassword;
13 |
14 | const updatedPasswordsState = await savePasswords(
15 | updatedPasswords,
16 | auth.email,
17 | auth.password
18 | );
19 |
20 | if (updatedPasswordsState) {
21 | return updatedPasswordsState;
22 | }
23 | }
24 | } catch (error) {
25 | console.log(error);
26 |
27 | return null;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------