From d83a4fd730569c88c77ace1030be464b3947ee16 Mon Sep 17 00:00:00 2001 From: Vichet97 Date: Sat, 16 Oct 2021 23:59:17 +0700 Subject: [PATCH 01/12] Fix build failed, Python not found --- Dockerfile | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6cc8e8c..10c9bfa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,13 @@ RUN yarn build # Regenerate node modules as production RUN rm -rf ./node_modules -RUN yarn install --production --frozen-lockfile +RUN apk --no-cache add cmake clang clang-dev make gcc g++ libc-dev linux-headers +RUN apk add --no-cache --virtual .gyp \ + python \ + make \ + g++ \ + && yarn install --production --frozen-lockfile \ + && apk del .gyp # Bundle stage FROM node:15-alpine AS production @@ -30,4 +36,4 @@ COPY --from=build /build/.next ./.next # Start script USER node EXPOSE 3000 -CMD ["yarn", "start:prod"] \ No newline at end of file +CMD ["yarn", "start:prod"] From 574271ef4cd0b482877d0d9be428d9fcbb421d66 Mon Sep 17 00:00:00 2001 From: Vichet97 Date: Sun, 17 Oct 2021 02:32:51 +0700 Subject: [PATCH 02/12] Fix unable to delete user after actions Set owner to `Deleted User` if deleted --- prisma/schema.prisma | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 568eac7..fed5739 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -32,9 +32,9 @@ model activity { id String @id @default(cuid()) action String ip String - owner String + owner String? @default("Deleted User") created DateTime @default(now()) - accounts accounts @relation(fields: [owner], references: [id], onDelete: SetNull) + accounts accounts? @relation(fields: [owner], references: [id], onDelete: SetDefault) } model certs { @@ -52,7 +52,7 @@ model certs { created DateTime @default(now()) updated DateTime @updatedAt domains domains @relation(fields: [domain], references: [id], onDelete: Cascade) - accounts accounts @relation(fields: [owner], references: [id], onDelete: SetNull) + accounts accounts @relation(fields: [owner], references: [id], onDelete: SetDefault) projects projects @relation(fields: [project], references: [id], onDelete: Cascade) } @@ -69,7 +69,7 @@ model databases { backup String? created DateTime @default(now()) updated DateTime @updatedAt - accounts accounts @relation(fields: [owner], references: [id], onDelete: SetNull) + accounts accounts @relation(fields: [owner], references: [id], onDelete: SetDefault) links links[] } @@ -85,10 +85,10 @@ model deployments { rollback Boolean @default(false) logs String? project String - owner String + owner String? @default("Deleted User") created DateTime @default(now()) updated DateTime @updatedAt - accounts accounts @relation(fields: [owner], references: [id], onDelete: SetNull) + accounts accounts? @relation(fields: [owner], references: [id], onDelete: SetDefault) projects projects @relation(fields: [project], references: [id], onDelete: Cascade) } @@ -96,10 +96,10 @@ model domains { id String @id @default(cuid()) domain String project String - owner String + owner String? @default("Deleted User") created DateTime @default(now()) updated DateTime @updatedAt - accounts accounts @relation(fields: [owner], references: [id], onDelete: SetNull) + accounts accounts? @relation(fields: [owner], references: [id], onDelete: SetDefault) projects projects @relation(fields: [project], references: [id], onDelete: Cascade) certs certs[] } @@ -109,10 +109,10 @@ model environment_variables { key String value String project String - owner String + owner String? @default("Deleted User") created DateTime @default(now()) updated DateTime @updatedAt - accounts accounts @relation(fields: [owner], references: [id], onDelete: SetNull) + accounts accounts? @relation(fields: [owner], references: [id], onDelete: SetDefault) projects projects @relation(fields: [project], references: [id], onDelete: Cascade) } @@ -134,7 +134,7 @@ model port_mappings { owner String created DateTime @default(now()) updated DateTime @updatedAt - accounts accounts @relation(fields: [owner], references: [id], onDelete: SetNull) + accounts accounts @relation(fields: [owner], references: [id], onDelete: SetDefault) projects projects @relation(fields: [project], references: [id], onDelete: Cascade) } @@ -148,7 +148,7 @@ model projects { created DateTime @default(now()) updated DateTime @updatedAt deleted DateTime? - accounts accounts @relation(fields: [owner], references: [id], onDelete: SetNull) + accounts accounts @relation(fields: [owner], references: [id], onDelete: SetDefault) certs certs[] deployments deployments[] domains domains[] From 9f3ab26512e26eb9d429a3bf35a13ff69e41ee7a Mon Sep 17 00:00:00 2001 From: Vichet97 Date: Sun, 17 Oct 2021 02:35:36 +0700 Subject: [PATCH 03/12] Fix unable to delete user after actions Set owner to `Deleted User` if deleted --- .../20211012052206_init/migration.sql | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/prisma/migrations/20211012052206_init/migration.sql b/prisma/migrations/20211012052206_init/migration.sql index 823e003..7a1d898 100644 --- a/prisma/migrations/20211012052206_init/migration.sql +++ b/prisma/migrations/20211012052206_init/migration.sql @@ -25,7 +25,7 @@ CREATE TABLE "activity" ( "id" TEXT NOT NULL, "action" TEXT NOT NULL, "ip" TEXT NOT NULL, - "owner" TEXT NOT NULL, + "owner" TEXT DEFAULT 'Deleted User', "created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "activity_pkey" PRIMARY KEY ("id") @@ -42,7 +42,7 @@ CREATE TABLE "certs" ( "logs" TEXT, "project" TEXT NOT NULL, "domain" TEXT NOT NULL, - "owner" TEXT NOT NULL, + "owner" TEXT DEFAULT 'Deleted User', "expires" TIMESTAMP(3), "created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated" TIMESTAMP(3) NOT NULL, @@ -55,7 +55,7 @@ CREATE TABLE "databases" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, "description" TEXT, - "owner" TEXT NOT NULL, + "owner" TEXT DEFAULT 'Deleted User', "type" "DatabaseType" NOT NULL, "version" TEXT NOT NULL, "status" TEXT NOT NULL, @@ -81,7 +81,7 @@ CREATE TABLE "deployments" ( "rollback" BOOLEAN NOT NULL DEFAULT false, "logs" TEXT, "project" TEXT NOT NULL, - "owner" TEXT NOT NULL, + "owner" TEXT DEFAULT 'Deleted User', "created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated" TIMESTAMP(3) NOT NULL, @@ -93,7 +93,7 @@ CREATE TABLE "domains" ( "id" TEXT NOT NULL, "domain" TEXT NOT NULL, "project" TEXT NOT NULL, - "owner" TEXT NOT NULL, + "owner" TEXT DEFAULT 'Deleted User', "created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated" TIMESTAMP(3) NOT NULL, @@ -106,7 +106,7 @@ CREATE TABLE "environment_variables" ( "key" TEXT NOT NULL, "value" TEXT NOT NULL, "project" TEXT NOT NULL, - "owner" TEXT NOT NULL, + "owner" TEXT DEFAULT 'Deleted User', "created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated" TIMESTAMP(3) NOT NULL, @@ -130,7 +130,7 @@ CREATE TABLE "port_mappings" ( "host" TEXT NOT NULL, "container" TEXT NOT NULL, "project" TEXT NOT NULL, - "owner" TEXT NOT NULL, + "owner" TEXT DEFAULT 'Deleted User', "created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated" TIMESTAMP(3) NOT NULL, @@ -144,7 +144,7 @@ CREATE TABLE "projects" ( "description" TEXT, "maintenance" BOOLEAN NOT NULL DEFAULT false, "origin" TEXT, - "owner" TEXT NOT NULL, + "owner" TEXT DEFAULT 'Deleted User', "created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated" TIMESTAMP(3) NOT NULL, "deleted" TIMESTAMP(3), @@ -159,34 +159,34 @@ CREATE UNIQUE INDEX "accounts_email_key" ON "accounts"("email"); CREATE UNIQUE INDEX "databases_dsn_key" ON "databases"("dsn"); -- AddForeignKey -ALTER TABLE "activity" ADD CONSTRAINT "activity_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "activity" ADD CONSTRAINT "activity_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET DEFAULT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "certs" ADD CONSTRAINT "certs_domain_fkey" FOREIGN KEY ("domain") REFERENCES "domains"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "certs" ADD CONSTRAINT "certs_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "certs" ADD CONSTRAINT "certs_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET DEFAULT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "certs" ADD CONSTRAINT "certs_project_fkey" FOREIGN KEY ("project") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "databases" ADD CONSTRAINT "databases_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "databases" ADD CONSTRAINT "databases_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET DEFAULT ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "deployments" ADD CONSTRAINT "deployments_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "deployments" ADD CONSTRAINT "deployments_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET DEFAULT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "deployments" ADD CONSTRAINT "deployments_project_fkey" FOREIGN KEY ("project") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "domains" ADD CONSTRAINT "domains_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "domains" ADD CONSTRAINT "domains_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET DEFAULT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "domains" ADD CONSTRAINT "domains_project_fkey" FOREIGN KEY ("project") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "environment_variables" ADD CONSTRAINT "environment_variables_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "environment_variables" ADD CONSTRAINT "environment_variables_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET DEFAULT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "environment_variables" ADD CONSTRAINT "environment_variables_project_fkey" FOREIGN KEY ("project") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE; @@ -198,10 +198,10 @@ ALTER TABLE "links" ADD CONSTRAINT "links_project_fkey" FOREIGN KEY ("project") ALTER TABLE "links" ADD CONSTRAINT "links_database_fkey" FOREIGN KEY ("database") REFERENCES "databases"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "port_mappings" ADD CONSTRAINT "port_mappings_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "port_mappings" ADD CONSTRAINT "port_mappings_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET DEFAULT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "port_mappings" ADD CONSTRAINT "port_mappings_project_fkey" FOREIGN KEY ("project") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "projects" ADD CONSTRAINT "projects_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "projects" ADD CONSTRAINT "projects_owner_fkey" FOREIGN KEY ("owner") REFERENCES "accounts"("id") ON DELETE SET DEFAULT ON UPDATE CASCADE; From 2622b24eb349fbdd86ed59455b399f69789b284e Mon Sep 17 00:00:00 2001 From: Vichet97 Date: Sun, 17 Oct 2021 11:43:08 +0700 Subject: [PATCH 04/12] Enable app health check --- pages/api/projects/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/pages/api/projects/index.ts b/pages/api/projects/index.ts index 66d78cc..5d4f969 100644 --- a/pages/api/projects/index.ts +++ b/pages/api/projects/index.ts @@ -36,6 +36,7 @@ export default async function (req, res) { }, }) await ssh('dokku', ['apps:create', projects.id]) + await ssh('dokku', ['checks:enable', projects.id]) await log(req, accountId, `New project created: ${name}`) res.status(201).json(projects) From 5c15b03add4d743c0b15bf5a80a281379c3b3ffb Mon Sep 17 00:00:00 2001 From: Vichet97 Date: Sun, 17 Oct 2021 11:47:22 +0700 Subject: [PATCH 05/12] Create Github webhook by protocol --- pages/settings.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pages/settings.tsx b/pages/settings.tsx index b3bcb82..72e9ca4 100644 --- a/pages/settings.tsx +++ b/pages/settings.tsx @@ -4,7 +4,8 @@ import { useApi, useValidSession } from '@hooks' import toast from 'react-hot-toast' import qrcode from 'qrcode' -export default function Settings({ host }) { +export default function Settings({scheme, host}) { + const baseUri = `${scheme}://${host}`; const [user, setUser] = useState({}) const [ips, setIps] = useState([]) const [mfa, setMfa] = useState(null) @@ -136,11 +137,11 @@ export default function Settings({ host }) { name: 'My Github Connection', public: false, request_oauth_on_install: true, - url: `http://${host}`, - redirect_url: `http://${host}/api/github/connect`, - callback_urls: [`http://${host}/api/github/connect`], + url: baseUri, + redirect_url: `${baseUri}/api/github/connect`, + callback_url: `${baseUri}/api/github/connect`, hook_attributes: { - url: `http://${host}/api/github/webhook`, + url: `${baseUri}/api/github/webhook`, active: true, }, default_permissions: { @@ -225,8 +226,11 @@ export default function Settings({ host }) { } export async function getServerSideProps(context) { + const {referer} = context.req.headers; + const scheme = context.req.headers['x-forwarded-proto'] || (referer && referer.includes("https://") ? "https": "http"); return { props: { + scheme, host: context.req.headers['host'] || null, }, ...useValidSession(context), From 5f274498da24705c47c3582b1f0cbab34d2dd7d3 Mon Sep 17 00:00:00 2001 From: Vichet97 Date: Sun, 17 Oct 2021 11:49:24 +0700 Subject: [PATCH 06/12] Create Github webhook by protocol --- pages/api/github/connect.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pages/api/github/connect.ts b/pages/api/github/connect.ts index 2b572f4..90e23d7 100644 --- a/pages/api/github/connect.ts +++ b/pages/api/github/connect.ts @@ -6,8 +6,11 @@ import axios from 'axios' export default async function (req, res) { try { if (req.method === 'GET') { - let { code, state } = req.query - let { host } = req.headers + let { code, state } = req.query; + let { host, referer } = req.headers; + + const scheme = req.headers['x-forwarded-proto'] || (referer && referer.includes("https://") ? "https": "http"); + const baseUri = `${scheme}://${host}`; if (!host) return res.status(409).send() @@ -36,7 +39,7 @@ export default async function (req, res) { }) res.redirect( - `https://github.com/login/oauth/authorize?client_id=${client_id}&redirect_uri=http://${host}/api/github/connect&scope=repo%20read:user&allow_signup=false&state=gh_authorize:${accountId}` + `https://github.com/login/oauth/authorize?client_id=${client_id}&redirect_uri=${baseUri}/api/github/connect&scope=repo%20read:user&allow_signup=false&state=gh_authorize:${accountId}` ) await ssh('dokku', ['config:set', 'admin', `GH_CLIENT_ID=${client_id}`, `GH_CLIENT_SECRET=${client_secret}`]) @@ -58,7 +61,7 @@ export default async function (req, res) { { client_id: (tokens as any).github.setup.client_id, client_secret: (tokens as any).github.setup.client_secret, - redirect_uri: `http://${host}/api/github/connect`, + redirect_uri: `${baseUri}/api/github/connect`, code: code, }, { From 45978939e2c41145a70f4e5863981aa5c82b6825 Mon Sep 17 00:00:00 2001 From: Vichet Date: Sun, 17 Oct 2021 12:23:53 +0700 Subject: [PATCH 07/12] Set default avatar --- lib/components/Nav.tsx | 2 +- pages/activity.tsx | 2 +- pages/admin.tsx | 2 +- pages/index.tsx | 2 +- public/icons/avatar-default.png | Bin 0 -> 50620 bytes 5 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 public/icons/avatar-default.png diff --git a/lib/components/Nav.tsx b/lib/components/Nav.tsx index f96455c..298b1c2 100644 --- a/lib/components/Nav.tsx +++ b/lib/components/Nav.tsx @@ -48,7 +48,7 @@ export function Nav({ active }) { style={{ backgroundSize: 'cover', backgroundPosition: 'center', - backgroundImage: `url(${user && user.avatar})`, + backgroundImage: `url(${user && user.avatar ? user.avatar : '/icons/avatar-default.png'})`, }} /> diff --git a/pages/activity.tsx b/pages/activity.tsx index 2caf659..bade5f3 100644 --- a/pages/activity.tsx +++ b/pages/activity.tsx @@ -28,7 +28,7 @@ export default function Activity() { return (
- +

{i.action}

diff --git a/pages/admin.tsx b/pages/admin.tsx index 56950d6..93d8e95 100644 --- a/pages/admin.tsx +++ b/pages/admin.tsx @@ -161,7 +161,7 @@ export default function Admin() { style={{ backgroundSize: 'cover', backgroundPosition: 'center', - backgroundImage: `url(${i.avatar})`, + backgroundImage: `url(${i.avatar || '/icons/avatar-default.png'})`, }} /> {i.name} {i.email}{' '} diff --git a/pages/index.tsx b/pages/index.tsx index 684567e..ade6c9f 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -110,7 +110,7 @@ export default function Home() { return (

- +

{i.action}

diff --git a/public/icons/avatar-default.png b/public/icons/avatar-default.png new file mode 100644 index 0000000000000000000000000000000000000000..0dbdf6d65037488175c9984e0a437c327be012eb GIT binary patch literal 50620 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 literal 0 HcmV?d00001 From e45108f52d6ade4c51a535fde92baf58e7280516 Mon Sep 17 00:00:00 2001 From: Vichet Date: Sun, 17 Oct 2021 15:33:37 +0700 Subject: [PATCH 08/12] - Show branch in app overview - Format to local datet if more than 60min --- lib/components/DeploymentTable.tsx | 6 +++--- lib/utils/date.tsx | 12 ++++++++++++ lib/utils/index.ts | 1 + package.json | 1 + pages/activity.tsx | 4 ++-- pages/admin.tsx | 4 ++-- pages/database/[id]/backups.tsx | 4 ++-- pages/database/[id]/index.tsx | 6 +++--- pages/index.tsx | 6 +++--- pages/project/[id]/deployments/[deployId].tsx | 10 +++++++--- pages/project/[id]/index.tsx | 13 ++++++++----- tsconfig.json | 1 + 12 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 lib/utils/date.tsx create mode 100644 lib/utils/index.ts diff --git a/lib/components/DeploymentTable.tsx b/lib/components/DeploymentTable.tsx index d6c377b..da837ef 100644 --- a/lib/components/DeploymentTable.tsx +++ b/lib/components/DeploymentTable.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import { useApi } from '@hooks' import { Spinner, Status } from '@components' -import * as timeago from 'timeago.js' +import {dateFormat} from '@utils' export function DeploymentTable({ id, setCount = (n) => {}, limit = 0 }) { const [deployments, setDeployments] = useState(null) @@ -28,11 +28,11 @@ export function DeploymentTable({ id, setCount = (n) => {}, limit = 0 }) { {i.message || `Deployment #${deployments.length - k}`} {i.rollback && } -

Deploying from {i.type}

+

Deploying from {i.type} {i.branch ? `(${i.branch})` : ''}

-

{timeago.format(i.created)}

+

{dateFormat(i.created)}

diff --git a/lib/utils/date.tsx b/lib/utils/date.tsx new file mode 100644 index 0000000..bd59ef4 --- /dev/null +++ b/lib/utils/date.tsx @@ -0,0 +1,12 @@ +import moment from 'moment'; +import * as timeago from 'timeago.js'; + +export function dateFormat(dateTime: Date) { + let formated = timeago.format(dateTime) + const now = moment(new Date()); + const then = moment(dateTime); + const diff = now.diff(then, 'minutes'); + if(diff > 60) + formated = then.local().format('DD/MM/YYYY hh:mm:ss A'); + return formated; +} \ No newline at end of file diff --git a/lib/utils/index.ts b/lib/utils/index.ts new file mode 100644 index 0000000..32cb75e --- /dev/null +++ b/lib/utils/index.ts @@ -0,0 +1 @@ +export * from './date' \ No newline at end of file diff --git a/package.json b/package.json index 456a29d..edfa91f 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dsn-parser": "^1.0.3", "js-cookie": "^3.0.1", "jsonwebtoken": "^8.5.1", + "moment": "^2.29.1", "next": "latest", "node-ssh": "^12.0.0", "otplib": "^12.0.1", diff --git a/pages/activity.tsx b/pages/activity.tsx index bade5f3..133ccce 100644 --- a/pages/activity.tsx +++ b/pages/activity.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react' import { Nav, Spinner } from '@components' import { useApi, useValidSession } from '@hooks' -import * as timeago from 'timeago.js' +import {dateFormat} from '@utils' export default function Activity() { const [activity, setActivity] = useState(null) @@ -36,7 +36,7 @@ export default function Activity() {

-

{timeago.format(i.created)}

+

{dateFormat(i.created)}

) })} diff --git a/pages/admin.tsx b/pages/admin.tsx index 93d8e95..04144eb 100644 --- a/pages/admin.tsx +++ b/pages/admin.tsx @@ -3,7 +3,7 @@ import { useApi, useValidSession } from '@hooks' import { Disclosure } from '@headlessui/react' import { Button, Nav, Input, Select } from '@components' import { useRouter } from 'next/router' -import * as timeago from 'timeago.js' +import {dateFormat} from '@utils' import toast from 'react-hot-toast' import cookie from 'js-cookie' @@ -176,7 +176,7 @@ export default function Admin() {

Has MFA Enabled

{i.mfa_enabled ? 'Yes' : 'No'}

Created

-

{timeago.format(i.created)}

+

{dateFormat(i.created)}


diff --git a/pages/database/[id]/backups.tsx b/pages/database/[id]/backups.tsx index d5de11a..b33a3c0 100644 --- a/pages/database/[id]/backups.tsx +++ b/pages/database/[id]/backups.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import { useRouter } from 'next/router' import { Button, Nav, Input, DatabaseSidebar } from '@components' import { useApi, useValidSession } from '@hooks' -import * as timeago from 'timeago.js' +import {dateFormat} from '@utils' import toast from 'react-hot-toast' import cronstrue from 'cronstrue' @@ -96,7 +96,7 @@ export default function Project() {

Status

{database.backup !== null ? 'Enabled' : 'Disabled'}

Initialized

-

{timeago.format(database.backup && database.backup.initialized)}

+

{dateFormat(database.backup && database.backup.initialized)}

Schedule

{humanCron(database.backup.schedule)}

Encrypted

diff --git a/pages/database/[id]/index.tsx b/pages/database/[id]/index.tsx index 2d2cf9d..2643f28 100644 --- a/pages/database/[id]/index.tsx +++ b/pages/database/[id]/index.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import { useRouter } from 'next/router' import { Status, Nav, DatabaseSidebar } from '@components' import { useApi, useValidSession } from '@hooks' -import * as timeago from 'timeago.js' +import {dateFormat} from '@utils' export default function Project() { const [database, setDatabase] = useState(null) @@ -34,7 +34,7 @@ export default function Project() { {database.name} -

Created {timeago.format(database.created)}

+

Created {dateFormat(database.created)}

@@ -52,7 +52,7 @@ export default function Project() {

Created

-

{timeago.format(database.created)}

+

{dateFormat(database.created)}

Type

diff --git a/pages/index.tsx b/pages/index.tsx index ade6c9f..851e9d7 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react' import { useApi, useValidSession } from '@hooks' import { Spinner, Nav, Button, Status } from '@components' -import * as timeago from 'timeago.js' +import {dateFormat} from '@utils' import Link from 'next/link' export default function Home() { @@ -50,7 +50,7 @@ export default function Home() { {i.domains.length !== 0 ? ( <>{i.domains[0].domain} ) : ( - <>Created {timeago.format(i.created)} + <>Created {dateFormat(i.created)} )}

@@ -118,7 +118,7 @@ export default function Home() {

-

{timeago.format(i.created)}

+

{dateFormat(i.created)}

) }) diff --git a/pages/project/[id]/deployments/[deployId].tsx b/pages/project/[id]/deployments/[deployId].tsx index d029d9f..f78fce6 100644 --- a/pages/project/[id]/deployments/[deployId].tsx +++ b/pages/project/[id]/deployments/[deployId].tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import { useInterval, useApi, useValidSession } from '@hooks' import { Status, Nav, ProjectSidebar, Spinner, Button } from '@components' import { useRouter } from 'next/router' -import * as timeago from 'timeago.js' +import {dateFormat} from '@utils' import toast from 'react-hot-toast' import ansi from 'ansi_up' @@ -70,7 +70,7 @@ export default function Deployments() {

Triggered by {deployment.type} - {deployment.manual && ' (Manual)'} {timeago.format(deployment.created)} + {deployment.manual && ' (Manual)'} {dateFormat(deployment.created)}

{deployment.status === 'COMPLETED' && ( @@ -96,7 +96,7 @@ export default function Deployments() {

Created

-

{timeago.format(deployment.created)}

+

{dateFormat(deployment.created)}

Buildpack Type

@@ -108,6 +108,10 @@ export default function Deployments() { {deployment.type || Unknown} {deployment.manual && '(Manual)'}

+
+

Branch

+

{deployment.branch || Unknown}

+

Project ID

{deployment.project}

diff --git a/pages/project/[id]/index.tsx b/pages/project/[id]/index.tsx index 384c1f8..42b1f10 100644 --- a/pages/project/[id]/index.tsx +++ b/pages/project/[id]/index.tsx @@ -2,19 +2,22 @@ import { useState, useEffect } from 'react' import { useRouter } from 'next/router' import { Status, Nav, ProjectSidebar, DeploymentTable } from '@components' import { useApi, useValidSession } from '@hooks' -import * as timeago from 'timeago.js' +import {dateFormat} from '@utils' export default function Project() { const [project, setProject] = useState(null) const [builds, setBuilds] = useState(null) + const [deployments, setDeployments] = useState(null) const router = useRouter() let { id } = router.query + let lastBulitBranch = deployments && deployments[0]?.branch; useEffect(() => { const hydrate = async () => { if (id) setProject(await useApi(`/api/projects/${id}`)) if (id) setBuilds(await useApi(`/api/projects/${id}/deployments`)) + if (id) setDeployments(await useApi(`/api/projects/${id}/deployments?take=1`)) } hydrate() }, [id]) @@ -38,7 +41,7 @@ export default function Project() { /> {project.name} -

Created {timeago.format(project.created)}

+

Created {dateFormat(project.created)}

@@ -56,8 +59,8 @@ export default function Project() {

-

Created

-

{timeago.format(project.created)}

+

Branch

+

{lastBulitBranch || Unknown}

Project ID

@@ -69,7 +72,7 @@ export default function Project() {
Latest Builds
- +
diff --git a/tsconfig.json b/tsconfig.json index 392b613..fc6704f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "baseUrl": ".", "paths": { "@components": ["lib/components"], + "@utils": ["lib/utils"], "@server/*": ["lib/server/*"], "@hooks": ["lib/hooks"] } From d14f327ff2d5f1114ae7c5f542e085db878e6892 Mon Sep 17 00:00:00 2001 From: Vichet Date: Sun, 17 Oct 2021 19:37:50 +0700 Subject: [PATCH 09/12] Git webhook & Fix rollback for null commit --- package.json | 1 + pages/api/git/webhook.ts | 92 +++++++++++++++++++ pages/project/[id]/deployments/[deployId].tsx | 4 +- pages/project/[id]/index.tsx | 38 +++++++- 4 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 pages/api/git/webhook.ts diff --git a/package.json b/package.json index edfa91f..d89e88b 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "prisma": "^3.1.1", "qrcode": "^1.4.4", "react": "^17.0.2", + "react-copy-to-clipboard": "^5.0.4", "react-dom": "^17.0.2", "react-hot-toast": "^2.1.1", "request-ip": "^2.1.3", diff --git a/pages/api/git/webhook.ts b/pages/api/git/webhook.ts new file mode 100644 index 0000000..8cf3709 --- /dev/null +++ b/pages/api/git/webhook.ts @@ -0,0 +1,92 @@ +import log from '@server/log' +import prisma from '@server/db' +import build from '@server/build' +import moment from 'moment' + +function allowDeployment(lastDeployment: Date) { + const now = moment(new Date()); + const then = moment(lastDeployment); + const diff = now.diff(then, 'minutes'); + return (diff > 5); +} + +export default async function (req, res) { + try { + if (req.method === 'POST') { + let {projectId, branch} = req.query; + if (!projectId) res.status(400).send(); + + let project = await prisma.projects.findFirst({ + where: { + id: projectId, + }, + include: { + accounts: true, + deployments: { + take: 1, + orderBy: { + created: 'desc', + } + } + } + }); + let lastDeployment = project?.deployments?.pop(); + + if (project && lastDeployment) { + let { ref, after, head_commit, commit, commits } = req.body; + let { origin } = lastDeployment; + + if (!head_commit) head_commit = (commit||(commits && commits[0])); + + if( !branch ) branch = (lastDeployment?.branch || 'master'); + + let commitedBranch = ref?.split('refs/heads/')[1]; + let message = head_commit?.message; + + if(lastDeployment?.type !== 'git' || !allowDeployment(lastDeployment.created) || (commitedBranch && branch !== commitedBranch)) { + return res.status(400).send(); + } else { + let deployment = await prisma.deployments.create({ + data: { + branch, + origin, + commit: after, + message, + type: 'git', + status: 'BUILDING', + manual: false, + projects: { + connect: { + id: project.id, + }, + }, + accounts: { + connect: { + id: project.accounts.id, + }, + }, + }, + }) + + await log( + req, + project.accounts.id, + `Deployment for ${project.name} was triggered on branch ${branch} by Git commit ${after}` + ) + + res.status(202).json(deployment) + + await build(project.id, deployment.id, origin, branch) + } + + } else { + res.status(400).send() + } + } else { + return res.status(405).send() + } + } catch (e) { + if (typeof e == 'undefined') return e + res.status(500).send() + } +} diff --git a/pages/project/[id]/deployments/[deployId].tsx b/pages/project/[id]/deployments/[deployId].tsx index f78fce6..6d2c540 100644 --- a/pages/project/[id]/deployments/[deployId].tsx +++ b/pages/project/[id]/deployments/[deployId].tsx @@ -73,7 +73,7 @@ export default function Deployments() { {deployment.manual && ' (Manual)'} {dateFormat(deployment.created)}

- {deployment.status === 'COMPLETED' && ( + {deployment.status === 'COMPLETED' && deployment.commit && (