diff --git a/.github/workflows/deploy-api.yml b/.github/workflows/deploy-api.yml index 280e8c8c..05635844 100644 --- a/.github/workflows/deploy-api.yml +++ b/.github/workflows/deploy-api.yml @@ -11,7 +11,7 @@ on: jobs: build: runs-on: ubuntu-latest - environment: ${{ github.ref == 'refs/heads/main' && 'beta' || 'stage' }} + environment: ${{ github.ref == 'refs/heads/main' && 'alpha' || 'stage' }} name: Build and push API docker image for release steps: @@ -32,59 +32,35 @@ jobs: REPOSITORY_NAME: api run: | # Build a docker container and push it to ACR - docker build -t $ACR_REGISTRY_URL/$REPOSITORY_NAME:${GITHUB_SHA::6} -t $ACR_REGISTRY_URL/$REPOSITORY_NAME:latest -f ./apps/web/Dockerfile . + docker build -t $ACR_REGISTRY_URL/$REPOSITORY_NAME:${GITHUB_SHA::6} -t $ACR_REGISTRY_URL/$REPOSITORY_NAME:latest -f ./apps/api/Dockerfile . echo "Pushing image to ACR..." docker push $ACR_REGISTRY_URL/$REPOSITORY_NAME:latest docker push $ACR_REGISTRY_URL/$REPOSITORY_NAME:${GITHUB_SHA::6} echo "name=image::$ACR_REGISTRY_URL/$REPOSITORY_NAME:latest" >> $GITHUB_OUTPUT - # setup-database: - # needs: build - # name: Setup Database - # runs-on: ubuntu-latest - # environment: alpha - - # steps: - # - name: Checkout - # uses: actions/checkout@v4 - - # - name: Install Node.js - # uses: actions/setup-node@v4 - # with: - # node-version: 20 - - # - name: Install pnpm - # uses: pnpm/action-setup@v4 - # with: - # version: 9.2.0 - # run_install: false - - # - name: Deploy migrations - # env: - # DATABASE_URL: ${{ secrets.DATABASE_URL }} - # run: pnpm db:deploy-migrations - - # deploy: - # needs: [build, setup-database] - # runs-on: ubuntu-latest - # environment: alpha - # name: Deploy API docker image for release - - # steps: - # - name: Checkout - # uses: actions/checkout@v4 - - # - name: Configure AWS credentials - # uses: aws-actions/configure-aws-credentials@v4 - # with: - # aws-access-key-id: ${{ secrets.ACCESS_KEY }} - # aws-secret-access-key: ${{ secrets.SECRET_KEY }} - # aws-region: ap-south-1 + deploy: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'alpha' || 'stage' }} + name: Restart API container app + needs: build + steps: + - name: Azure Login action + uses: azure/login@v2 + with: + creds: ${{ secrets.CONTAINER_APP_SP_CREDENTIALS }} + enable-AzPSSession: true - # - name: Force re-deploy task in service - # id: force-redeploy - # env: - # ECS_CLUSTER: ${{ vars.ECS_CLUSTER }} - # ECS_SERVICE: ${{ vars.ECS_API_SERVICE }} - # run: | - # aws ecs update-service --cluster $ECS_CLUSTER --service $ECS_SERVICE --force-new-deployment + - name: Azure CLI script + uses: azure/cli@v2 + env: + API_CONTAINER: ${{ vars.API_CONTAINER }} + API_CONTAINER_RG: ${{ vars.API_CONTAINER_RG }} + ACR_REGISTRY_URL: ${{ vars.ACR_REGISTRY_URL }} + REPOSITORY_NAME: api + with: + azcliversion: latest + inlineScript: | + az containerapp update \ + --name $API_CONTAINER \ + --resource-group $API_CONTAINER_RG \ + --image $ACR_REGISTRY_URL/$REPOSITORY_NAME:latest diff --git a/.github/workflows/deploy-platform.yml b/.github/workflows/deploy-platform.yml index 53d98b55..66b77fd7 100644 --- a/.github/workflows/deploy-platform.yml +++ b/.github/workflows/deploy-platform.yml @@ -16,7 +16,7 @@ on: jobs: build: runs-on: ubuntu-latest - environment: ${{ github.ref == 'refs/heads/main' && 'beta' || 'stage' }} + environment: ${{ github.ref == 'refs/heads/main' && 'alpha' || 'stage' }} name: Build and push Platform docker image for release steps: @@ -44,26 +44,23 @@ jobs: echo "name=image::$ACR_REGISTRY_URL/$REPOSITORY_NAME:latest" >> $GITHUB_OUTPUT # deploy: - # needs: build # runs-on: ubuntu-latest - # environment: alpha - # name: Deploy Platform docker image for release - + # environment: ${{ github.ref == 'refs/heads/main' && 'alpha' || 'stage' }} + # name: Restart Platform container app + # needs: build # steps: - # - name: Checkout - # uses: actions/checkout@v4 - - # - name: Configure AWS credentials - # uses: aws-actions/configure-aws-credentials@v4 + # - name: Azure Login action + # uses: azure/login@v2 # with: - # aws-access-key-id: ${{ secrets.ACCESS_KEY }} - # aws-secret-access-key: ${{ secrets.SECRET_KEY }} - # aws-region: ap-south-1 + # creds: ${{ secrets.CONTAINER_APP_SP_CREDENTIALS }} + # enable-AzPSSession: true - # - name: Force re-deploy task in service - # id: force-redeploy + # - name: Azure CLI script + # uses: azure/cli@v2 # env: - # ECS_CLUSTER: ${{ vars.ECS_CLUSTER }} - # ECS_SERVICE: ${{ vars.ECS_PLATFORM_SERVICE }} - # run: | - # aws ecs update-service --cluster $ECS_CLUSTER --service $ECS_SERVICE --force-new-deployment + # PLATFORM_CONTAINER: ${{ vars.PLATFORM_CONTAINER }} + # PLATFORM_CONTAINER_RG: ${{ vars.PLATFORM_CONTAINER_RG }} + # with: + # azcliversion: latest + # inlineScript: | + # az container restart --name $PLATFORM_CONTAINER --resource-group $PLATFORM_CONTAINER_RG diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 9cb2d184..aabb0761 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -11,7 +11,7 @@ on: jobs: build: runs-on: ubuntu-latest - environment: ${{ github.ref == 'refs/heads/main' && 'beta' || 'stage' }} + environment: ${{ github.ref == 'refs/heads/main' && 'alpha' || 'stage' }} name: Build and push Web docker image for release steps: @@ -38,27 +38,29 @@ jobs: docker push $ACR_REGISTRY_URL/$REPOSITORY_NAME:${GITHUB_SHA::6} echo "name=image::$ACR_REGISTRY_URL/$REPOSITORY_NAME:latest" >> $GITHUB_OUTPUT - # deploy: - # needs: build - # runs-on: ubuntu-latest - # environment: alpha - # name: Deploy Web docker image for release - - # steps: - # - name: Checkout - # uses: actions/checkout@v4 - - # - name: Configure AWS credentials - # uses: aws-actions/configure-aws-credentials@v4 - # with: - # aws-access-key-id: ${{ secrets.ACCESS_KEY }} - # aws-secret-access-key: ${{ secrets.SECRET_KEY }} - # aws-region: ap-south-1 + deploy: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'alpha' || 'stage' }} + name: Restart Web container app + needs: build + steps: + - name: Azure Login action + uses: azure/login@v2 + with: + creds: ${{ secrets.CONTAINER_APP_SP_CREDENTIALS }} + enable-AzPSSession: true - # - name: Force re-deploy task in service - # id: force-redeploy - # env: - # ECS_CLUSTER: ${{ vars.ECS_CLUSTER }} - # ECS_SERVICE: ${{ vars.ECS_WEB_SERVICE }} - # run: | - # aws ecs update-service --cluster $ECS_CLUSTER --service $ECS_SERVICE --force-new-deployment + - name: Azure CLI script + uses: azure/cli@v2 + env: + WEB_CONTAINER: ${{ vars.WEB_CONTAINER }} + WEB_CONTAINER_RG: ${{ vars.WEB_CONTAINER_RG }} + ACR_REGISTRY_URL: ${{ vars.ACR_REGISTRY_URL }} + REPOSITORY_NAME: web + with: + azcliversion: latest + inlineScript: | + az containerapp update \ + --name $WEB_CONTAINER \ + --resource-group $WEB_CONTAINER_RG \ + --image $ACR_REGISTRY_URL/$REPOSITORY_NAME:latest diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 88ce4c87..836fa57c 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -31,6 +31,8 @@ USER node FROM node:20-alpine AS prod +RUN apk add --no-cache openssl + # Don't run production as root USER node @@ -42,6 +44,6 @@ COPY --chown=root:root --chmod=755 --from=build /app/node_modules /app/node_modu COPY --chown=root:root --chmod=755 --from=build /app/apps/api/node_modules /app/apps/api/node_modules COPY --chown=root:root --chmod=755 --from=build /app/apps/api/dist /app/apps/api/dist -EXPOSE ${API_PORT} +EXPOSE 4200 ENTRYPOINT ["node", "/app/apps/api/dist/main.js"] \ No newline at end of file diff --git a/apps/api/src/prisma/schema.prisma b/apps/api/src/prisma/schema.prisma index 2c2ea940..126557a9 100644 --- a/apps/api/src/prisma/schema.prisma +++ b/apps/api/src/prisma/schema.prisma @@ -1,5 +1,6 @@ generator client { provider = "prisma-client-js" + binaryTargets = ["native", "linux-musl"] } datasource db { diff --git a/apps/platform/Dockerfile b/apps/platform/Dockerfile index 69f9c239..1072d540 100644 --- a/apps/platform/Dockerfile +++ b/apps/platform/Dockerfile @@ -44,9 +44,9 @@ COPY --from=installer --chown=nextjs:nodejs /app/apps/platform/.next ./apps/pla COPY --from=installer --chown=nextjs:nodejs /app/apps/platform/public ./apps/platform/public -ENV PORT=3025 +ENV PORT=3000 ENV NEXT_SHARP_PATH=/app/apps/platform/.next/sharp -EXPOSE 3025 +EXPOSE 3000 # keyshade-ignore ENV HOSTNAME "0.0.0.0" diff --git a/apps/platform/public/svg/shared/Error.svg b/apps/platform/public/svg/shared/Error.svg new file mode 100644 index 00000000..6b5ae5ea --- /dev/null +++ b/apps/platform/public/svg/shared/Error.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/platform/public/svg/shared/Vector.svg b/apps/platform/public/svg/shared/Vector.svg new file mode 100644 index 00000000..c7aad118 --- /dev/null +++ b/apps/platform/public/svg/shared/Vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/platform/public/svg/shared/index.ts b/apps/platform/public/svg/shared/index.ts index 07133a8c..0d09dde2 100644 --- a/apps/platform/public/svg/shared/index.ts +++ b/apps/platform/public/svg/shared/index.ts @@ -7,6 +7,9 @@ import SettingsSVG from './settings.svg' import ThreeDotOptionSVG from './3dotOption.svg' import AddSVG from './add.svg' import LoadingSVG from './loading.svg' +import MessageSVG from './message.svg' +import VectorSVG from './vector.svg' +import ErrorSVG from './Error.svg' export { DropdownSVG, @@ -17,5 +20,8 @@ export { SettingsSVG, ThreeDotOptionSVG, AddSVG, - LoadingSVG + LoadingSVG, + MessageSVG, + VectorSVG, + ErrorSVG } diff --git a/apps/platform/public/svg/shared/message.svg b/apps/platform/public/svg/shared/message.svg new file mode 100644 index 00000000..f7757498 --- /dev/null +++ b/apps/platform/public/svg/shared/message.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/platform/src/app/(main)/page.tsx b/apps/platform/src/app/(main)/page.tsx index 9a9cb2ac..af66c101 100644 --- a/apps/platform/src/app/(main)/page.tsx +++ b/apps/platform/src/app/(main)/page.tsx @@ -38,6 +38,7 @@ import { DialogTrigger } from '@/components/ui/dialog' import ControllerInstance from '@/lib/controller-instance' +import { Textarea } from '@/components/ui/textarea' export default function Index(): JSX.Element { const [isSheetOpen, setIsSheetOpen] = useState(false) @@ -144,10 +145,12 @@ export default function Index(): JSX.Element { - + {isProjectEmpty ? null : ( + + )}
@@ -190,8 +193,8 @@ export default function Index(): JSX.Element { > Description - { setNewProjectData((prev) => ({ @@ -217,7 +220,10 @@ export default function Index(): JSX.Element { onChange={(e) => { setNewProjectData((prev) => ({ ...prev, - envName: e.target.value + environments: (prev.environments || []).map( + (env, index) => + index === 0 ? { ...env, name: e.target.value } : env + ) })) }} placeholder="Your project default environment name" @@ -232,13 +238,18 @@ export default function Index(): JSX.Element { > Env. Description - { setNewProjectData((prev) => ({ ...prev, - envDescription: e.target.value + environments: (prev.environments || []).map( + (env, index) => + index === 0 + ? { ...env, description: e.target.value } + : env + ) })) }} placeholder="Detailed description about your environment" @@ -265,7 +276,7 @@ export default function Index(): JSX.Element { })) }} > - + @@ -334,7 +345,9 @@ export default function Index(): JSX.Element {
Create a file and start setting up your environment and secret keys
- +
)} diff --git a/apps/platform/src/app/(main)/project/[project]/@variable/page.tsx b/apps/platform/src/app/(main)/project/[project]/@variable/page.tsx index a4362de4..cf4ac3a1 100644 --- a/apps/platform/src/app/(main)/project/[project]/@variable/page.tsx +++ b/apps/platform/src/app/(main)/project/[project]/@variable/page.tsx @@ -1,7 +1,171 @@ -import React from 'react' +'use client' -function VariablePage(): React.JSX.Element { - return
VariablePage
+import { useEffect, useState } from 'react' +import { Button } from '@/components/ui/button' +import { + ClientResponse, + GetAllVariablesOfProjectResponse, + Project, +} from '@keyshade/schema' +import { FolderSVG } from '@public/svg/dashboard' +import { MessageSVG } from '@public/svg/shared' +import { ChevronDown } from 'lucide-react' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table' +import ControllerInstance from '@/lib/controller-instance' + +interface VariablePageProps { + currentProject: Project | undefined +} + + +function VariablePage({ + currentProject +}: VariablePageProps): React.JSX.Element { + + const [allVariables, setAllVariables] = useState([]) + // Holds the currently open section ID + const [openSections, setOpenSections] = useState>(new Set()) + + //Environments table toggle logic + const toggleSection = (id: string) => { + setOpenSections((prev) => { + const newSet = new Set(prev) + if (newSet.has(id)) { + newSet.delete(id) + } else { + newSet.add(id) + } + return newSet + }) + } + + useEffect(() => { + + const getAllVariables = async () => { + + if (!currentProject) { + return + } + + const { success, error, data }: ClientResponse = + await ControllerInstance.getInstance().variableController.getAllVariablesOfProject( + { projectSlug: currentProject.slug }, + {} + ) + + if (success && data) { + setAllVariables(data.items) + } else { + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) + } + } + + getAllVariables() + }, [currentProject]) + + return ( +
+ + {/* Showing this when there are no variables present */} + {allVariables.length === 0 ? ( +
+ + +
+

+ Declare your first variable +

+

+ Declare and store a variable against different environments +

+
+ + +
+ ) : ( + // Showing this when variables are present +
+ {allVariables.map((variable) => ( + toggleSection(variable.variable.id)} + className="w-full" + > + +
+ + {variable.variable.name} + + +
+
+
+
+ {(() => { + const days = Math.ceil(Math.abs(new Date().getTime() - new Date(variable.variable.createdAt).getTime()) / (1000 * 60 * 60 * 24)); + return `${days} ${days === 1 ? 'day' : 'days'} ago by`; + })()} +
+
+
+ {variable.variable.lastUpdatedBy.name.split(' ')[0]} +
+ + + + {variable.variable.lastUpdatedBy.name.charAt(0).toUpperCase() + variable.variable.lastUpdatedBy.name.slice(1, 2).toLowerCase()} + + +
+
+ +
+
+ + {variable.values ? ( + + + + Environment + Value + + + + {variable.values.map((env) => ( + + + {env.environment.name} + + + {env.value} + + + ))} + +
+ ) : ( + <> + )} +
+
+ ))} +
+ )} +
+ ) } export default VariablePage diff --git a/apps/platform/src/app/(main)/project/[project]/layout.tsx b/apps/platform/src/app/(main)/project/[project]/layout.tsx index 9edfb0bb..36114df1 100644 --- a/apps/platform/src/app/(main)/project/[project]/layout.tsx +++ b/apps/platform/src/app/(main)/project/[project]/layout.tsx @@ -1,8 +1,16 @@ 'use client' +import type { MouseEvent, MouseEventHandler } from 'react' import { useEffect, useState } from 'react' import { useSearchParams } from 'next/navigation' import { AddSVG } from '@public/svg/shared' -import type { Project } from '@keyshade/schema' +import type { + ClientResponse, + CreateVariableRequest, + Environment, + GetAllEnvironmentsOfProjectResponse, + Project +} from '@keyshade/schema' +import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Dialog, @@ -15,6 +23,15 @@ import { import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import ControllerInstance from '@/lib/controller-instance' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select' +import VariablePage from './@variable/page' +import { Toaster } from '@/components/ui/sonner' interface DetailedProjectPageProps { params: { project: string } @@ -31,12 +48,76 @@ function DetailedProjectPage({ const [key, setKey] = useState('') // eslint-disable-next-line @typescript-eslint/no-unused-vars -- will be used later const [value, setValue] = useState('') - const [currentProject, setCurrentProject] = useState() + const [isOpen, setIsOpen] = useState(false) + const [newVariableData, setNewVariableData] = useState({ + variableName: '', + note: '', + environmentName: '', + environmentValue: '' + }) + const [availableEnvironments, setAvailableEnvironments] = useState([]) const searchParams = useSearchParams() const tab = searchParams.get('tab') ?? 'rollup-details' + const addVariable = async (e: MouseEvent) => { + e.preventDefault() + + if (!currentProject) { + throw new Error("Current project doesn't exist") + } + + const request: CreateVariableRequest = { + name: newVariableData.variableName, + projectSlug: currentProject.slug, + entries: newVariableData.environmentValue + ? [ + { + value: newVariableData.environmentValue, + environmentSlug: newVariableData.environmentName + } + ] + : undefined, + note: newVariableData.note + } + + const { success, error } = + await ControllerInstance.getInstance().variableController.createVariable( + request, + {} + ) + + if (success) { + toast.success('Variable added successfully', { + // eslint-disable-next-line react/no-unstable-nested-components -- we need to nest the description + description: () => ( +

+ The variable has been added to the project +

+ ) + }) + } + + if (error) { + if (error.statusCode === 409) { + toast.error('Variable name already exists', { + // eslint-disable-next-line react/no-unstable-nested-components -- we need to nest the description + description: () => ( +

+ Variable name is already there, kindly use different one. +

+ ) + }) + } else { + // eslint-disable-next-line no-console -- we need to log the error that are not in the if condition + console.error(error) + } + } + + setIsOpen(false) + } + useEffect(() => { async function getProjectBySlug() { const { success, error, data } = @@ -56,67 +137,228 @@ function DetailedProjectPage({ getProjectBySlug() }, [params.project]) + useEffect(() => { + const getAllEnvironments = async () => { + if (!currentProject) { + return + } + + const { + success, + error, + data + }: ClientResponse = + await ControllerInstance.getInstance().environmentController.getAllEnvironmentsOfProject( + { projectSlug: currentProject.slug }, + {} + ) + + if (success && data) { + setAvailableEnvironments(data.items) + } else { + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) + } + } + + getAllEnvironments() + }, [currentProject]) + return (
-
+
{currentProject?.name}
- - - - - - - Add a new secret - - Add a new secret to the project. This secret will be encrypted - and stored securely. - - -
-
-
- - { - setKey(e.target.value) - }} - placeholder="Enter the name of the secret" - /> + {tab === 'secret' && ( + + + + + + + Add a new secret + + Add a new secret to the project. This secret will be encrypted + and stored securely. + + +
+
+
+ + { + setKey(e.target.value) + }} + placeholder="Enter the name of the secret" + /> +
+
+ + { + setValue(e.target.value) + }} + placeholder="Enter the value of the secret" + /> +
-
- - { - setValue(e.target.value) - }} - placeholder="Enter the value of the secret" - /> +
+
-
- + +
+ )} + {tab === 'variable' && ( + + + + + + + + Add a new variable + + + Add a new variable to the project + + + +
+
+
+ + + setNewVariableData({ + ...newVariableData, + variableName: e.target.value + }) + } + placeholder="Enter the key of the variable" + value={newVariableData.variableName} + /> +
+ +
+ + + setNewVariableData({ + ...newVariableData, + note: e.target.value + }) + } + placeholder="Enter the note of the secret" + value={newVariableData.note} + /> +
+ +
+
+ + +
+ +
+ + + setNewVariableData({ + ...newVariableData, + environmentValue: e.target.value + }) + } + placeholder="Environment Value" + value={newVariableData.environmentValue} + /> +
+
+ +
+ +
+
-
- -
+ +
+ )} -
+ +
{tab === 'secret' && secret} - {tab === 'variable' && variable} + {tab === 'variable' && } + {/* {tab === 'variable' && variable} */}
+ ) } -export default DetailedProjectPage +export default DetailedProjectPage \ No newline at end of file diff --git a/apps/platform/src/components/dashboard/projectCard/index.tsx b/apps/platform/src/components/dashboard/projectCard/index.tsx index 4cd0ad52..1818c51f 100644 --- a/apps/platform/src/components/dashboard/projectCard/index.tsx +++ b/apps/platform/src/components/dashboard/projectCard/index.tsx @@ -25,6 +25,7 @@ function ProjectCard({ }: ProjectCardProps): JSX.Element { const { id, + slug, name, description, environmentCount, @@ -69,7 +70,7 @@ function ProjectCard({
diff --git a/apps/platform/src/components/ui/collapsible.tsx b/apps/platform/src/components/ui/collapsible.tsx new file mode 100644 index 00000000..28dd1567 --- /dev/null +++ b/apps/platform/src/components/ui/collapsible.tsx @@ -0,0 +1,12 @@ +"use client" + +import * as React from "react" +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } \ No newline at end of file diff --git a/apps/platform/src/components/ui/textarea.tsx b/apps/platform/src/components/ui/textarea.tsx new file mode 100644 index 00000000..2a10e86f --- /dev/null +++ b/apps/platform/src/components/ui/textarea.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' + +import { cn } from '@/lib/utils' + +const Textarea = React.forwardRef< + HTMLTextAreaElement, + React.ComponentProps<'textarea'> +>(({ className, ...props }, ref) => { + return ( +