diff --git a/.github/workflows/api.yaml b/.github/workflows/api.yaml index 9e48ac2a..685cc3bc 100644 --- a/.github/workflows/api.yaml +++ b/.github/workflows/api.yaml @@ -23,8 +23,21 @@ jobs: with: version: 8 run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- - - name: Install packages + - name: Install packages run: | pnpm i @@ -32,6 +45,7 @@ jobs: run: | pnpm run lint:api - - name: Test + - name: Test run: | + pnpm run db:generate-types pnpm run test:api diff --git a/.github/workflows/cli.yaml b/.github/workflows/cli.yaml new file mode 100644 index 00000000..a074572b --- /dev/null +++ b/.github/workflows/cli.yaml @@ -0,0 +1,52 @@ +on: + push: + paths: ['apps/cli/**', '.github/workflows/cli.yaml'] + pull_request: + paths: ['apps/cli/**'] + +jobs: + validate: + runs-on: ubuntu-latest + name: Validate + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install packages + run: | + pnpm i + + - name: Lint + run: | + pnpm run lint:cli + + - name: Test + run: | + pnpm run test:cli + + diff --git a/.github/workflows/pr-lint.yaml b/.github/workflows/pr-lint.yaml index 3b5a8e39..4768b417 100644 --- a/.github/workflows/pr-lint.yaml +++ b/.github/workflows/pr-lint.yaml @@ -1,17 +1,23 @@ -name: PR Lint +name: "Lint PR" on: pull_request: - types: [opened, edited, reopened, synchronize] + types: + - opened + - edited + - synchronize + pull_request_target: + types: + - opened + - edited + - synchronize jobs: - pr-lint: + lint-pr: + name: Validate PR title runs-on: ubuntu-latest steps: - - uses: morrisoncole/pr-lint-action@v1.7.0 - with: - repo-token: '${{ secrets.GITHUB_TOKEN }}' - title-regex: '^(fix|feat|chore|docs|style|refactor|perf|test|ci|build|breaking-change|revert):\s(?:\D)[\w\d\s\-\.\,\_]+$' - on-failed-regex-fail-action: true - on-failed-regex-create-review: true - on-failed-regex-comment: 'PR title must match: `%regex%`!' + - name: Lint PR + uses: amannn/action-semantic-pull-request@v4.6.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sdk-node.yaml b/.github/workflows/sdk-node.yaml new file mode 100644 index 00000000..27716230 --- /dev/null +++ b/.github/workflows/sdk-node.yaml @@ -0,0 +1,50 @@ +on: + push: + paths: ['packages/sdk-node/**', '.github/workflows/sdk-node.yaml'] + pull_request: + paths: ['packages/sdk-node'] + +jobs: + validate: + runs-on: ubuntu-latest + name: Validate + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install packages + run: | + pnpm i + + - name: Lint + run: | + pnpm run lint:sdk-node + + - name: Test + run: | + pnpm run test:sdk-node diff --git a/.github/workflows/web.yaml b/.github/workflows/web.yaml index ba9412f8..1f3ce74d 100644 --- a/.github/workflows/web.yaml +++ b/.github/workflows/web.yaml @@ -24,6 +24,19 @@ jobs: version: 8 run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Install packages run: | pnpm i diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..87aec0b7 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +support@keyshade.xyz. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..2a8a39fc --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported versions + +We always recommend using the latest version of Keyshade to ensure you get all security updates. + +## Reporting vulnerabilities + +Please do not file GitHub issues or post on our public forum for security vulnerabilities, as they are public! + +Keyshade takes security issues very seriously. If you have any concerns about Keyshade or believe you have uncovered a vulnerability, please get in touch via the e-mail address support@keyshade.xyz. In the message, try to provide a description of the issue and ideally a way of reproducing it. The security team will get back to you as soon as possible. + +Note that this security address should be used only for undisclosed vulnerabilities. Please report any security problems to us before disclosing it publicly. diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts new file mode 100644 index 00000000..c0b6904a --- /dev/null +++ b/apps/api/src/auth/auth.service.ts @@ -0,0 +1,79 @@ +import { + HttpException, + HttpStatus, + Inject, + Injectable, + Logger, + LoggerService +} from '@nestjs/common' +import { PrismaRepository } from '../prisma/prisma.repository' +import { randomUUID } from 'crypto' +import { JwtService } from '@nestjs/jwt' +import { UserAuthenticatedResponse } from './auth.types' +import { + IResendService, + RESEND_SERVICE +} from '../resend/services/resend.service.interface' + +@Injectable() +export class AuthService { + private readonly OTP_EXPIRY = 5 * 60 * 1000 // 5 minutes + private readonly logger: LoggerService + + constructor( + private repository: PrismaRepository, + @Inject(RESEND_SERVICE) private resend: IResendService, + private jwt: JwtService + ) { + this.logger = new Logger(AuthService.name) + } + + async sendOtp(email: string): Promise { + if (!email || !email.includes('@')) { + this.logger.error(`Invalid email address: ${email}`) + throw new HttpException( + 'Please enter a valid email address', + HttpStatus.BAD_REQUEST + ) + } + + // We need to create the user if it doesn't exist yet + if (!(await this.repository.findUserByEmail(email))) { + await this.repository.createUser(email) + } + + const otp = await this.repository.createOtp( + email, + randomUUID().slice(0, 6).toUpperCase(), + this.OTP_EXPIRY + ) + + await this.resend.sendOtp(email, otp.code) + this.logger.log(`Login code sent to ${email}: ${otp.code}`) + } + + async validateOtp( + email: string, + otp: string + ): Promise { + const user = await this.repository.findUserByEmail(email) + if (!user) { + this.logger.error(`User not found: ${email}`) + throw new HttpException('User not found', HttpStatus.NOT_FOUND) + } + + if (!(await this.repository.isOtpValid(email, otp))) { + this.logger.error(`Invalid login code for ${email}: ${otp}`) + throw new HttpException('Invalid login code', HttpStatus.UNAUTHORIZED) + } + + await this.repository.deleteOtp(email, otp) + + this.logger.log(`User logged in: ${email}`) + + return { + ...user, + token: await this.jwt.signAsync({ id: user.id }) + } + } +} diff --git a/apps/api/src/auth/guard/admin.guard.ts b/apps/api/src/auth/guard/admin.guard.ts index 6f99c6fb..30ea6a80 100644 --- a/apps/api/src/auth/guard/admin.guard.ts +++ b/apps/api/src/auth/guard/admin.guard.ts @@ -4,7 +4,9 @@ import { Observable } from 'rxjs' @Injectable() export class AdminGuard implements CanActivate { - canActivate(context: ExecutionContext): boolean | Promise | Observable { + canActivate( + context: ExecutionContext + ): boolean | Promise | Observable { const request = context.switchToHttp().getRequest() const user: User = request.user diff --git a/apps/api/src/common/mock-data/users.ts b/apps/api/src/common/mock-data/users.ts new file mode 100644 index 00000000..011b7404 --- /dev/null +++ b/apps/api/src/common/mock-data/users.ts @@ -0,0 +1,40 @@ +import { User } from '@prisma/client' + +export const users: Array = [ + { + id: '1', + name: 'Cristobal Hessel', + email: 'cristobal@keyshade.xyz', + profilePictureUrl: 'https://keyshade.xyz/cristobal.jpg', + isActive: true, + isOnboardingFinished: false, + isAdmin: false + }, + { + id: '2', + name: 'John Doe', + email: 'johndoe@keyshade.xyz', + profilePictureUrl: 'https://keyshade.xyz/johndoe.jpg', + isActive: true, + isOnboardingFinished: false, + isAdmin: false + }, + { + id: '3', + name: 'Jocelyn Larkin', + email: 'jocelyn@keyshade.xyz', + profilePictureUrl: 'https://keyshade.xyz/jocelyn.jpg', + isActive: false, + isOnboardingFinished: false, + isAdmin: false + }, + { + id: '4', + name: 'Kadin Stiedemann', + email: 'kadin@keyshade.xyz', + profilePictureUrl: 'https://keyshade.xyz/kadin.jpg', + isActive: true, + isOnboardingFinished: false, + isAdmin: true + } +] diff --git a/apps/api/src/decorators/user.decorator.ts b/apps/api/src/decorators/user.decorator.ts index bb32a758..0f122a72 100644 --- a/apps/api/src/decorators/user.decorator.ts +++ b/apps/api/src/decorators/user.decorator.ts @@ -1,9 +1,11 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common' import { User as DBUser } from '@prisma/client' -export const CurrentUser = createParamDecorator( - (_: unknown, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest() - return request.user - } -) +export const CurrentUser = createParamDecorator< + unknown, + ExecutionContext, + DBUser +>((_: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest() + return request.user +}) diff --git a/apps/api/src/prisma/prisma.repository.ts b/apps/api/src/prisma/prisma.repository.ts new file mode 100644 index 00000000..4ae4c568 --- /dev/null +++ b/apps/api/src/prisma/prisma.repository.ts @@ -0,0 +1,212 @@ +import { Injectable } from '@nestjs/common' +import { PrismaService } from './prisma.service' +import { Otp, User } from '@prisma/client' + +@Injectable() +export class PrismaRepository { + constructor(private prisma: PrismaService) {} + + /** + * Find a user by email + * @param email the email to search for + * @returns the user if found, null otherwise + */ + async findUserByEmail(email: string): Promise { + return await this.prisma.user.findUnique({ + where: { + email + } + }) + } + + /** + * Find a user by the user id + * @param id The id of the user to find + * @returns the user if found, null otherwise + */ + async findUserById(id: string): Promise { + return await this.prisma.user.findUnique({ + where: { + id + } + }) + } + + /** + * Find all users + * @param page The page number + * @param limit The number of items per page + * @param sort The field to sort by + * @param order The order to sort by + * @param search The search string + * @returns The list of users + */ + async findUsers( + page: number, + limit: number, + sort: string, + order: string, + search: string + ): Promise { + return await this.prisma.user.findMany({ + skip: (page - 1) * limit, + take: limit, + orderBy: { + [sort]: order + }, + where: { + OR: [ + { + name: { + contains: search + } + }, + { + email: { + contains: search + } + } + ] + } + }) + } + + /** + * Create a user with the given email. The onboarding process + * will aim at updating the user further. + * @param email The email of the user to create + * @returns + */ + async createUser(email: string): Promise { + return await this.prisma.user.create({ + data: { + email + } + }) + } + + /** + * Update an existing user + * @param id ID of the user to update + * @param data The data to update + * @returns The updated user + */ + async updateUser(id: string, data: Partial): Promise { + return await this.prisma.user.update({ + where: { + id + }, + data + }) + } + + /** + * Delete a user by id + * @param id The id of the user to delete + * @returns The deleted user + */ + async deleteUser(id: string): Promise { + return await this.prisma.user.delete({ + where: { + id + } + }) + } + + /** + * An OTP is valid if it exists, is not expired, and is associated with the given email + * @param email the email against which to check the OTP + * @param otp the OTP code to check + * @returns returns true if the OTP is valid, false otherwise + */ + async isOtpValid(email: string, otp: string): Promise { + const timeNow = new Date() + return ( + (await this.prisma.otp.count({ + where: { + code: otp, + user: { + email + }, + expiresAt: { + gt: timeNow + } + } + })) > 0 + ) + } + + async createOtp( + email: string, + otp: string, + expiresAfter: number + ): Promise { + const timeNow = new Date() + await this.invalidateOldOtps(email) + return await this.prisma.otp.create({ + data: { + code: otp, + expiresAt: new Date(timeNow.getTime() + expiresAfter), + user: { + connect: { + email + } + } + } + }) + } + + /** + * Invalidate Old OTPs for a User + * + * This method invalidates old OTPs (One-Time Passwords) associated with a user. + * It finds and deletes OTPs that belong to the user and have not expired yet. + * + * @param email - The email address of the user for whom old OTPs should be invalidated. + * + * @example + * ```typescript + * await invalidateOldOtps('user@example.com'); + * ``` + */ + private async invalidateOldOtps(email: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { + email + } + }) + + if (user) { + await this.prisma.otp.deleteMany({ + where: { + userId: user.id, + expiresAt: { + gte: new Date() + } + } + }) + } + } + + async deleteOtp(email: string, otp: string): Promise { + await this.prisma.otp.delete({ + where: { + code: otp, + AND: { + user: { + email + } + } + } + }) + } + + async excludeFields( + key: T, + ...fields: K[] + ): Promise> { + return Object.fromEntries( + Object.entries(key).filter(([k]) => !fields.includes(k as K)) + ) as Partial + } +} diff --git a/apps/api/src/user/dto/update.user/update.user.spec.ts b/apps/api/src/user/dto/update.user/update.user.spec.ts index 49133470..406d6d7f 100644 --- a/apps/api/src/user/dto/update.user/update.user.spec.ts +++ b/apps/api/src/user/dto/update.user/update.user.spec.ts @@ -1,7 +1,7 @@ -import { UpdateUserDto } from './update.user'; +import { UpdateUserDto } from './update.user' describe('UpdateUser', () => { it('should be defined', () => { - expect(new UpdateUserDto()).toBeDefined(); - }); -}); + expect(new UpdateUserDto()).toBeDefined() + }) +}) diff --git a/apps/api/src/user/dto/update.user/update.user.ts b/apps/api/src/user/dto/update.user/update.user.ts index 131062c8..b14ee3a7 100644 --- a/apps/api/src/user/dto/update.user/update.user.ts +++ b/apps/api/src/user/dto/update.user/update.user.ts @@ -1,7 +1,7 @@ -import { IsOptional, IsString } from "class-validator"; +import { IsOptional, IsString } from 'class-validator' export class UpdateUserDto { - @IsString() - @IsOptional() - name: string; + @IsString() + @IsOptional() + name: string } diff --git a/apps/api/src/user/repository/mock.repository.ts b/apps/api/src/user/repository/mock.repository.ts index 16a8c8a4..57cddab3 100644 --- a/apps/api/src/user/repository/mock.repository.ts +++ b/apps/api/src/user/repository/mock.repository.ts @@ -1,29 +1,51 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { User } from '@prisma/client' import { IUserRepository } from './interface.repository' +import { users } from '../../common/mock-data/users' export class MockUserRepository implements IUserRepository { findUserByEmail(email: string): Promise { throw new Error('Method not implemented.') } + findUserById(id: string): Promise { throw new Error('Method not implemented.') } - findUsers( + + async findUsers( page: number, limit: number, sort: string, order: string, search: string ): Promise { - throw new Error('Method not implemented.') + let user_data = [...users] + user_data.sort((a, b) => { + const comparison = a[sort].localeCompare(b[sort]) + return order === 'desc' ? -comparison : comparison + }) + + if (search.trim() !== '') { + user_data = user_data.filter((user) => + user.name.toLowerCase().includes(search.toLowerCase()) + ) + } + + const start_idx = (page - 1) * limit + const end_idx = start_idx + limit + user_data = user_data.slice(start_idx, end_idx) + + return Promise.resolve(user_data) } + createUser(email: string): Promise { throw new Error('Method not implemented.') } + deleteUser(id: string): Promise { throw new Error('Method not implemented.') } + async updateUser(id: string, data: User): Promise { return Promise.resolve({ id, diff --git a/apps/api/tsconfig.spec.json b/apps/api/tsconfig.spec.json index f6d8ffcc..9b2a121d 100644 --- a/apps/api/tsconfig.spec.json +++ b/apps/api/tsconfig.spec.json @@ -5,5 +5,10 @@ "module": "commonjs", "types": ["jest", "node"] }, - "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] } diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 6d7df09c..cc44de69 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -13,28 +13,27 @@ function Index() {
-
+
-
-
-
+
+
+

keyshade.xyz

- -
-

+

+

Manage all your secrets securely with public key encryption and realtime based tools, that seamlessly fits into your codebase

-
+
-
+
diff --git a/apps/web/components/Links.tsx b/apps/web/components/Links.tsx index 7d1318b4..0acdfd40 100644 --- a/apps/web/components/Links.tsx +++ b/apps/web/components/Links.tsx @@ -25,7 +25,7 @@ const Links = ({ icon, description, link }: LinksProps) => { className='unselectable' draggable='false' /> -

{description}

+

{description}

) diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..cd43f7f7 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,29 @@ +--- +description: Get to know about what we are building +--- + +# ❔ What is keyshade? + +## The problem + +With the rise in security concerns, software products are being made with security as their top concern. One of the most implemented strategy is to move all the secrets that the application is using to some cloud provider, where the secrets are fetched in runtime. By doing this, applications can separate their code from their configurations, ensuring both privacy and customizability. + +But, this comes with a cost of heavy human effort. Consider this scenario: Your application is using Google OAuth to sign in its users. Now, some day, you decided to use a new email account to log in users. At the very least, you have to make these changes: + +* **Update your keys** in the runtime environment +* **Restart your servers** to pick up the change in configuration + +Along with this, you are also required to store your **sensitive data** as plain text on servers, which you own indirectly! + +## The solution + +This is what led to the development of **keyshade**. keyshade is a secret-management tool that aims to make secret management a breeze, while keeping developer compatibility and integration as its top-most priority. + +With keyshade integrated into your codebase, you don't need to worry about leaking your secrets, or micromanaging them! Here are a few points as to why keyshade would an opt-in solution for you: + +* We use **Public Key Cryptography** at our very core. For every project that you make, you are allowed to specify a **private key** that stays in your custody forever (until you specify otherwise!). We then use a **public key** generated from your private key to encrypt and store your secrets. +* Your secrets are safe! We encrypt your data both **in transition** and **at rest**, making it mathematically impossible to sniff or tamper. +* Our SDKs are developed with **real-time** experience in mind. Feel like you need to change that API key of yours? We've got you covered! All you need to do is update it in keyshade, and leave the rest to the robots! +* **Collaborating** with others on a keyshade project is as secure as it gets! None of the collaborators will be able to see the value (not even the hashed up gibberish!) of any secret, but they will be easily able to add or update the values as and when they want to. Your private key stays safe with you! +* You are the person in command! We have put a lot of effort in developing a fine-tuned system, that allows you to micromanage the roles that you give to other users or even your own API keys. +* Lastly, we allow you to rotate your secrets periodically. This is an opt-in feature, should you want that JWT secret of yours to be regenerated so that bad folks don't breach your systems! diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 00000000..822de279 --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,14 @@ +# Table of contents + +* [❔ What is keyshade?](README.md) + +## 🥰 CONTRIBUTING TO KEYSHADE + +* [Summary](contributing-to-keyshade/summary.md) +* [Prerequisites](contributing-to-keyshade/prerequisites.md) +* [Environment Variables](contributing-to-keyshade/environment-variables.md) +* [Setting things up](contributing-to-keyshade/setting-things-up.md) +* [Running things locally](contributing-to-keyshade/running-things-locally/README.md) + * [Running the API](contributing-to-keyshade/running-things-locally/running-the-api.md) +* [Design of our code](contributing-to-keyshade/design-of-our-code/README.md) + * [API](contributing-to-keyshade/design-of-our-code/api.md) diff --git a/docs/contributing-to-keyshade/design-of-our-code/README.md b/docs/contributing-to-keyshade/design-of-our-code/README.md new file mode 100644 index 00000000..de2d11f5 --- /dev/null +++ b/docs/contributing-to-keyshade/design-of-our-code/README.md @@ -0,0 +1,13 @@ +--- +description: Get to know about the core building blocks of our code +--- + +# Design of our code + +## Brief + +When it comes to developing for us, we follow very strict norms! From proper indentation to using design patterns, we do an extensive coverage. But this is really difficult for anyone to grab randomly. So we decided to compile how each of our modules undergo development and the structure they follow. + +## Compilation of designs + +* [API](api.md) diff --git a/docs/contributing-to-keyshade/design-of-our-code/api.md b/docs/contributing-to-keyshade/design-of-our-code/api.md new file mode 100644 index 00000000..be0b8e35 --- /dev/null +++ b/docs/contributing-to-keyshade/design-of-our-code/api.md @@ -0,0 +1,33 @@ +--- +description: Design of our API +--- + +# API + +This document covers how we have developed our API, the stacks, and things you should know before you get started with it! + +## Stack + +Our API is developed using the following stack: + +* **NestJS** as the base +* **Prisma** as the DDL and DML +* **Supabase** as the database and bucket storage +* **Resend** as the mail agent + +## Structure + +As per the NestJS convention, our API base is totally modularized, with each module catering to a particular part of our project. The general module structure is as follows: + +* **controller**: Stores the APIs that the clients will be interacting with. +* **service**: Holds the business logic +* **repository**: Holds the logic related to manipulating the database +* **misc**: Holds utility functions and classes localized to the particular module +* **dto**: Contains class objects for data intake from the clients + +### The `prisma` module + +This module deserves special attention since it deals with the data layer. Apart from the usual files, we have two other: + +* `schema.prisma`: This contains the data definition +* `migrations`: This folder stores the migrations generated by running the `pnpm run db:generate-migrations` command. These migrations are the state that the current prisma database is in. diff --git a/docs/contributing-to-keyshade/environment-variables.md b/docs/contributing-to-keyshade/environment-variables.md new file mode 100644 index 00000000..d58a52cc --- /dev/null +++ b/docs/contributing-to-keyshade/environment-variables.md @@ -0,0 +1,17 @@ +--- +description: Get to know the environment you are working with +--- + +# Environment Variables + +## .env.example + +Here's the description of the environment variables used in the project. You can find the values for these variables in \`.env.example\`. + +* **DATABASE\_URL**: The URL of the PSQL database to connect to. This is used by the [Prisma Client](https://www.prisma.io/docs/orm/prisma-client) to connect to the database. +* **SUPABASE\_API\_URL**: The URL of the Supabase API. This is used by the [Supabase Client](https://supabase.io/docs/reference/javascript/supabase-client) to connect to the Supabase API. Make sure you create a Supabase project and get the API URL from the project settings. +* **SUPABASE\_ANON\_KEY**: The anonymous key of the Supabase project. This is used by the Supabase Client to connect to the Supabase API. Make sure you create a Supabase project and get the anonymous key from the project settings. +* **RESEND\_API\_KEY**: The API key for the [Resend API](https://resend-api.vercel.app/). The project uses Resend API to send out emails. +* **JWT\_SECRET**: The secret used to sign the JWT tokens. It is insignificant in the development environment. +* **FROM\_EMAIL**: The email address from which the emails will be sent. This is used by the Resend API to send out emails. +* **WEB\_FRONTEND\_URL, WORKSPACE\_FRONTEND\_URL**: The URLs of the web and workspace frontend respectively. These are used in the emails sometimes and in other spaces of the application too. diff --git a/docs/contributing-to-keyshade/prerequisites.md b/docs/contributing-to-keyshade/prerequisites.md new file mode 100644 index 00000000..664e6155 --- /dev/null +++ b/docs/contributing-to-keyshade/prerequisites.md @@ -0,0 +1,7 @@ +# Prerequisites + +Of course, to get started, you will need to have the required components in place. Gladly, there isn't much that needs to be done: + +* Make sure you have Git installed and have an account in GitHub +* Have an account in Supabase along with a project +* Have an account in Resend diff --git a/docs/contributing-to-keyshade/running-things-locally/README.md b/docs/contributing-to-keyshade/running-things-locally/README.md new file mode 100644 index 00000000..0874b44f --- /dev/null +++ b/docs/contributing-to-keyshade/running-things-locally/README.md @@ -0,0 +1,16 @@ +--- +description: Compiles a list of article to help running the services locally +--- + +# Running things locally + +This document (and the sub-series) guides you on how you can run and test each of the applications on your local device. There are two categories of projects: + +* The main ones under the apps directory +* The examples of the SDKs + +## Run locally + +You can pick up any of these topics from the following and start with it. + +* [Running the API](running-the-api.md) diff --git a/docs/contributing-to-keyshade/running-things-locally/running-the-api.md b/docs/contributing-to-keyshade/running-things-locally/running-the-api.md new file mode 100644 index 00000000..612fe77e --- /dev/null +++ b/docs/contributing-to-keyshade/running-things-locally/running-the-api.md @@ -0,0 +1,43 @@ +--- +description: Get to know how you can develop the API! +--- + +# Running the API + +The API resides in the `apps/api` directory. It is a NestJS project. To run the API locally, do the following: + +* Generate the prisma types: + +```bash +pnpm run db:generate-types +``` + +* Deploy the migrations: + +```bash +pnpm run db:deploy-migrations +``` + +* Start the server in development mode: + +```bash +pnpm run dev:api +``` + +* Once you have made the changes and added tests (if any), make sure to test the code: + +```bash +pnpm run test:api +``` + +* Lint the code: + +```bash +pnpm run lint:api +``` + +* Run prettier: + +```bash +pnpm run prettier:fix:api +``` diff --git a/docs/contributing-to-keyshade/setting-things-up.md b/docs/contributing-to-keyshade/setting-things-up.md new file mode 100644 index 00000000..afe23c4b --- /dev/null +++ b/docs/contributing-to-keyshade/setting-things-up.md @@ -0,0 +1,44 @@ +# Setting things up + +## Setting up the .env file + +Make a copy of the `.env.example` file and rename it to `.env` + +```bash +cp .env.example .env +``` + +Fill in the values for the environment variables in the `.env` file. You can find the values for the variables in the [Environment Variables](environment-variables.md) section. + +## Setting up `pnpm` + +keyshade works with any version of **node (>=18)** and takes the liberty that you have it installed. The project uses `pnpm` as the package manager. To install `pnpm`, run the following command: + +```bash +npm install -g pnpm +``` + +{% hint style="info" %} +For Linux users, in case the above command fails with permission error, try running this: + +```bash +sudo npm install -g pnpm +``` +{% endhint %} + +## Installing the dependencies + +To install the dependencies, run the following command: + +```bash +pnpm install +``` + +## Installing NX + +The last step is to install NX. It is the monorepo management tool that we are using. Read more about it in [https://nx.dev](https://nx.dev). To install nx, you need to run the following command: + +```bash +pnpm i -g nx +``` + diff --git a/docs/contributing-to-keyshade/summary.md b/docs/contributing-to-keyshade/summary.md new file mode 100644 index 00000000..5c7eebe1 --- /dev/null +++ b/docs/contributing-to-keyshade/summary.md @@ -0,0 +1,9 @@ +--- +description: Be a part of the keyshade today! +--- + +# Summary + +We are an [open-source](https://github.com/keyshade-xyz/keyshade) organization. We solely rely upon the contributions made from the peer of developers out there, and we are really thankful towards them. Even the smallest of contributions (changing the name of a variable, fixing typos) are very much appreciated. + +This series of documents aim at setting up and developing keyshade in your device locally. Start anywhere you feel like!