From 143cd46a81b7f20cb55bf643f358f0554229919a Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Sun, 11 Sep 2022 11:51:43 +0000 Subject: [PATCH] feat: Add queue reset button --- apps/api/src/lib/common.ts | 2 +- apps/api/src/routes/api/v1/handlers.ts | 590 ++++++++++-------- apps/api/src/routes/api/v1/index.ts | 6 +- .../applications/[id]/logs/build.svelte | 21 +- apps/ui/src/routes/index.svelte | 4 +- package.json | 2 +- 6 files changed, 346 insertions(+), 279 deletions(-) diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 55bfb08ab..530f95aa4 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -21,7 +21,7 @@ import { scheduler } from './scheduler'; import { supportedServiceTypesAndVersions } from './services/supportedVersions'; import { includeServices } from './services/common'; -export const version = '3.10.1'; +export const version = '3.10.2'; export const isDev = process.env.NODE_ENV === 'development'; const algorithm = 'aes-256-ctr'; diff --git a/apps/api/src/routes/api/v1/handlers.ts b/apps/api/src/routes/api/v1/handlers.ts index 4e877f13c..3bdeeca0a 100644 --- a/apps/api/src/routes/api/v1/handlers.ts +++ b/apps/api/src/routes/api/v1/handlers.ts @@ -1,296 +1,340 @@ - -import axios from 'axios'; -import { compareVersions } from 'compare-versions'; -import cuid from 'cuid'; -import bcrypt from 'bcryptjs'; -import { asyncExecShell, asyncSleep, cleanupDockerStorage, errorHandler, isDev, listSettings, prisma, uniqueName, version } from '../../../lib/common'; -import { supportedServiceTypesAndVersions } from '../../../lib/services/supportedVersions'; -import type { FastifyReply, FastifyRequest } from 'fastify'; -import type { Login, Update } from '.'; -import type { GetCurrentUser } from './types'; +import axios from "axios"; +import { compareVersions } from "compare-versions"; +import cuid from "cuid"; +import bcrypt from "bcryptjs"; +import { + asyncExecShell, + asyncSleep, + cleanupDockerStorage, + errorHandler, + isDev, + listSettings, + prisma, + uniqueName, + version, +} from "../../../lib/common"; +import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions"; +import { scheduler } from "../../../lib/scheduler"; +import type { FastifyReply, FastifyRequest } from "fastify"; +import type { Login, Update } from "."; +import type { GetCurrentUser } from "./types"; export async function hashPassword(password: string): Promise { - const saltRounds = 15; - return bcrypt.hash(password, saltRounds); + const saltRounds = 15; + return bcrypt.hash(password, saltRounds); } export async function cleanupManually(request: FastifyRequest) { - try { - const { serverId } = request.body; - const destination = await prisma.destinationDocker.findUnique({ where: { id: serverId } }) - await cleanupDockerStorage(destination.id, true, true) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { serverId } = request.body; + const destination = await prisma.destinationDocker.findUnique({ + where: { id: serverId }, + }); + await cleanupDockerStorage(destination.id, true, true); + return {}; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function checkUpdate(request: FastifyRequest) { - try { - const isStaging = request.hostname === 'staging.coolify.io' || request.hostname === 'arm.coolify.io' - const currentVersion = version; - const { data: versions } = await axios.get( - `https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}` - ); - const latestVersion = versions['coolify'].main.version - const isUpdateAvailable = compareVersions(latestVersion, currentVersion); - if (isStaging) { - return { - isUpdateAvailable: true, - latestVersion: 'next' - } - } - return { - isUpdateAvailable: isStaging ? true : isUpdateAvailable === 1, - latestVersion - }; - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const isStaging = + request.hostname === "staging.coolify.io" || + request.hostname === "arm.coolify.io"; + const currentVersion = version; + const { data: versions } = await axios.get( + `https://get.coollabs.io/versions.json?appId=${process.env["COOLIFY_APP_ID"]}&version=${currentVersion}` + ); + const latestVersion = versions["coolify"].main.version; + const isUpdateAvailable = compareVersions(latestVersion, currentVersion); + if (isStaging) { + return { + isUpdateAvailable: true, + latestVersion: "next", + }; + } + return { + isUpdateAvailable: isStaging ? true : isUpdateAvailable === 1, + latestVersion, + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function update(request: FastifyRequest) { - const { latestVersion } = request.body; - try { - if (!isDev) { - const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); - await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); - await asyncExecShell(`env | grep COOLIFY > .env`); - await asyncExecShell( - `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` - ); - await asyncExecShell( - `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"` - ); - return {}; - } else { - await asyncSleep(2000); - return {}; - } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + const { latestVersion } = request.body; + try { + if (!isDev) { + const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); + await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); + await asyncExecShell(`env | grep COOLIFY > .env`); + await asyncExecShell( + `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` + ); + await asyncExecShell( + `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"` + ); + return {}; + } else { + await asyncSleep(2000); + return {}; + } + } catch ({ status, message }) { + return errorHandler({ status, message }); + } +} +export async function resetQueue(request: FastifyRequest) { + try { + const teamId = request.user.teamId; + if (teamId === "0") { + await prisma.build.updateMany({ + where: { status: { in: ["queued", "running"] } }, + data: { status: "canceled" }, + }); + scheduler.workers.get("deployApplication").postMessage("cancel"); + } + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function restartCoolify(request: FastifyRequest) { - try { - const teamId = request.user.teamId; - if (teamId === '0') { - if (!isDev) { - asyncExecShell(`docker restart coolify`); - return {}; - } else { - return {}; - } - } - throw { status: 500, message: 'You are not authorized to restart Coolify.' }; - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const teamId = request.user.teamId; + if (teamId === "0") { + if (!isDev) { + asyncExecShell(`docker restart coolify`); + return {}; + } else { + return {}; + } + } + throw { + status: 500, + message: "You are not authorized to restart Coolify.", + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function showDashboard(request: FastifyRequest) { - try { - const userId = request.user.userId; - const teamId = request.user.teamId; - const applications = await prisma.application.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { settings: true, destinationDocker: true, teams: true } - }); - const databases = await prisma.database.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { settings: true, destinationDocker: true, teams: true } - }); - const services = await prisma.service.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { destinationDocker: true, teams: true } - }); - const gitSources = await prisma.gitSource.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { teams: true } - }); - const destinations = await prisma.destinationDocker.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { teams: true } - }); - const settings = await listSettings(); - return { - applications, - databases, - services, - gitSources, - destinations, - settings, - }; - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const userId = request.user.userId; + const teamId = request.user.teamId; + const applications = await prisma.application.findMany({ + where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, + include: { settings: true, destinationDocker: true, teams: true }, + }); + const databases = await prisma.database.findMany({ + where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, + include: { settings: true, destinationDocker: true, teams: true }, + }); + const services = await prisma.service.findMany({ + where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, + include: { destinationDocker: true, teams: true }, + }); + const gitSources = await prisma.gitSource.findMany({ + where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, + include: { teams: true }, + }); + const destinations = await prisma.destinationDocker.findMany({ + where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, + include: { teams: true }, + }); + const settings = await listSettings(); + return { + applications, + databases, + services, + gitSources, + destinations, + settings, + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } -export async function login(request: FastifyRequest, reply: FastifyReply) { - if (request.user) { - return reply.redirect('/dashboard'); - } else { - const { email, password, isLogin } = request.body || {}; - if (!email || !password) { - throw { status: 500, message: 'Email and password are required.' }; - } - const users = await prisma.user.count(); - const userFound = await prisma.user.findUnique({ - where: { email }, - include: { teams: true, permission: true }, - rejectOnNotFound: false - }); - if (!userFound && isLogin) { - throw { status: 500, message: 'User not found.' }; - } - const { isRegistrationEnabled, id } = await prisma.setting.findFirst() - let uid = cuid(); - let permission = 'read'; - let isAdmin = false; +export async function login( + request: FastifyRequest, + reply: FastifyReply +) { + if (request.user) { + return reply.redirect("/dashboard"); + } else { + const { email, password, isLogin } = request.body || {}; + if (!email || !password) { + throw { status: 500, message: "Email and password are required." }; + } + const users = await prisma.user.count(); + const userFound = await prisma.user.findUnique({ + where: { email }, + include: { teams: true, permission: true }, + rejectOnNotFound: false, + }); + if (!userFound && isLogin) { + throw { status: 500, message: "User not found." }; + } + const { isRegistrationEnabled, id } = await prisma.setting.findFirst(); + let uid = cuid(); + let permission = "read"; + let isAdmin = false; - if (users === 0) { - await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } }); - uid = '0'; - } - if (userFound) { - if (userFound.type === 'email') { - if (userFound.password === 'RESETME') { - const hashedPassword = await hashPassword(password); - if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) { - if (userFound.id === '0') { - await prisma.user.update({ - where: { email: userFound.email }, - data: { password: 'RESETME' } - }); - } else { - await prisma.user.update({ - where: { email: userFound.email }, - data: { password: 'RESETTIMEOUT' } - }); - } + if (users === 0) { + await prisma.setting.update({ + where: { id }, + data: { isRegistrationEnabled: false }, + }); + uid = "0"; + } + if (userFound) { + if (userFound.type === "email") { + if (userFound.password === "RESETME") { + const hashedPassword = await hashPassword(password); + if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) { + if (userFound.id === "0") { + await prisma.user.update({ + where: { email: userFound.email }, + data: { password: "RESETME" }, + }); + } else { + await prisma.user.update({ + where: { email: userFound.email }, + data: { password: "RESETTIMEOUT" }, + }); + } - throw { - status: 500, - message: 'Password reset link has expired. Please request a new one.' - }; - } else { - await prisma.user.update({ - where: { email: userFound.email }, - data: { password: hashedPassword } - }); - return { - userId: userFound.id, - teamId: userFound.id, - permission: userFound.permission, - isAdmin: true - }; - } - } + throw { + status: 500, + message: + "Password reset link has expired. Please request a new one.", + }; + } else { + await prisma.user.update({ + where: { email: userFound.email }, + data: { password: hashedPassword }, + }); + return { + userId: userFound.id, + teamId: userFound.id, + permission: userFound.permission, + isAdmin: true, + }; + } + } - const passwordMatch = await bcrypt.compare(password, userFound.password); - if (!passwordMatch) { - throw { - status: 500, - message: 'Wrong password or email address.' - }; - } - uid = userFound.id; - isAdmin = true; - } - } else { - permission = 'owner'; - isAdmin = true; - if (!isRegistrationEnabled) { - throw { - status: 404, - message: 'Registration disabled by administrator.' - }; - } - const hashedPassword = await hashPassword(password); - if (users === 0) { - await prisma.user.create({ - data: { - id: uid, - email, - password: hashedPassword, - type: 'email', - teams: { - create: { - id: uid, - name: uniqueName(), - destinationDocker: { connect: { network: 'coolify' } } - } - }, - permission: { create: { teamId: uid, permission: 'owner' } } - }, - include: { teams: true } - }); - } else { - await prisma.user.create({ - data: { - id: uid, - email, - password: hashedPassword, - type: 'email', - teams: { - create: { - id: uid, - name: uniqueName() - } - }, - permission: { create: { teamId: uid, permission: 'owner' } } - }, - include: { teams: true } - }); - } - } - return { - userId: uid, - teamId: uid, - permission, - isAdmin - }; - } + const passwordMatch = await bcrypt.compare( + password, + userFound.password + ); + if (!passwordMatch) { + throw { + status: 500, + message: "Wrong password or email address.", + }; + } + uid = userFound.id; + isAdmin = true; + } + } else { + permission = "owner"; + isAdmin = true; + if (!isRegistrationEnabled) { + throw { + status: 404, + message: "Registration disabled by administrator.", + }; + } + const hashedPassword = await hashPassword(password); + if (users === 0) { + await prisma.user.create({ + data: { + id: uid, + email, + password: hashedPassword, + type: "email", + teams: { + create: { + id: uid, + name: uniqueName(), + destinationDocker: { connect: { network: "coolify" } }, + }, + }, + permission: { create: { teamId: uid, permission: "owner" } }, + }, + include: { teams: true }, + }); + } else { + await prisma.user.create({ + data: { + id: uid, + email, + password: hashedPassword, + type: "email", + teams: { + create: { + id: uid, + name: uniqueName(), + }, + }, + permission: { create: { teamId: uid, permission: "owner" } }, + }, + include: { teams: true }, + }); + } + } + return { + userId: uid, + teamId: uid, + permission, + isAdmin, + }; + } } -export async function getCurrentUser(request: FastifyRequest, fastify) { - let token = null - const { teamId } = request.query - try { - const user = await prisma.user.findUnique({ - where: { id: request.user.userId } - }) - if (!user) { - throw "User not found"; - } - } catch (error) { - throw { status: 401, message: error }; - } - if (teamId) { - try { - const user = await prisma.user.findFirst({ - where: { id: request.user.userId, teams: { some: { id: teamId } } }, - include: { teams: true, permission: true } - }) - if (user) { - const permission = user.permission.find(p => p.teamId === teamId).permission - const payload = { - ...request.user, - teamId, - permission: permission || null, - isAdmin: permission === 'owner' || permission === 'admin' - - } - token = fastify.jwt.sign(payload) - } - - } catch (error) { - // No new token -> not switching teams - } - } - return { - settings: await prisma.setting.findFirst(), - supportedServiceTypesAndVersions, - token, - ...request.user - } +export async function getCurrentUser( + request: FastifyRequest, + fastify +) { + let token = null; + const { teamId } = request.query; + try { + const user = await prisma.user.findUnique({ + where: { id: request.user.userId }, + }); + if (!user) { + throw "User not found"; + } + } catch (error) { + throw { status: 401, message: error }; + } + if (teamId) { + try { + const user = await prisma.user.findFirst({ + where: { id: request.user.userId, teams: { some: { id: teamId } } }, + include: { teams: true, permission: true }, + }); + if (user) { + const permission = user.permission.find( + (p) => p.teamId === teamId + ).permission; + const payload = { + ...request.user, + teamId, + permission: permission || null, + isAdmin: permission === "owner" || permission === "admin", + }; + token = fastify.jwt.sign(payload); + } + } catch (error) { + // No new token -> not switching teams + } + } + return { + settings: await prisma.setting.findFirst(), + supportedServiceTypesAndVersions, + token, + ...request.user, + }; } diff --git a/apps/api/src/routes/api/v1/index.ts b/apps/api/src/routes/api/v1/index.ts index 52310998d..bab30236b 100644 --- a/apps/api/src/routes/api/v1/index.ts +++ b/apps/api/src/routes/api/v1/index.ts @@ -1,5 +1,5 @@ import { FastifyPluginAsync } from 'fastify'; -import { checkUpdate, login, showDashboard, update, showUsage, getCurrentUser, cleanupManually, restartCoolify } from './handlers'; +import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers'; import { GetCurrentUser } from './types'; export interface Update { @@ -47,6 +47,10 @@ const root: FastifyPluginAsync = async (fastify): Promise => { onRequest: [fastify.authenticate] }, async (request) => await restartCoolify(request)); + fastify.post('/internal/resetQueue', { + onRequest: [fastify.authenticate] + }, async (request) => await resetQueue(request)); + fastify.post('/internal/cleanup', { onRequest: [fastify.authenticate] }, async (request) => await cleanupManually(request)); diff --git a/apps/ui/src/routes/applications/[id]/logs/build.svelte b/apps/ui/src/routes/applications/[id]/logs/build.svelte index faeec72c0..6999f53f0 100644 --- a/apps/ui/src/routes/applications/[id]/logs/build.svelte +++ b/apps/ui/src/routes/applications/[id]/logs/build.svelte @@ -24,8 +24,9 @@ export let buildCount: any; import { page } from '$app/stores'; +import {addToast} from '$lib/store'; import BuildLog from './_BuildLog.svelte'; - import { get } from '$lib/api'; + import { get, post } from '$lib/api'; import { t } from '$lib/translations'; import { changeQueryParams, dateOptions, errorNotification } from '$lib/common'; import Tooltip from '$lib/components/Tooltip.svelte'; @@ -83,6 +84,21 @@ buildId = build; return changeQueryParams(buildId); } + async function resetQueue() { + const sure = confirm('It will reset all build queues for all applications. If something is queued, it will be canceled automatically. Are you sure? '); + if (sure) { + + try { + await post(`/internal/resetQueue`, {}); + addToast({ + message: 'Queue reset done.', + type: 'success' + }); + } catch (error) { + return errorNotification(error); + } + } + }
@@ -138,6 +154,7 @@
+
{#each builds as build, index (build.id)}
5}
-
diff --git a/apps/ui/src/routes/index.svelte b/apps/ui/src/routes/index.svelte index c0620a42e..673390ffd 100644 --- a/apps/ui/src/routes/index.svelte +++ b/apps/ui/src/routes/index.svelte @@ -32,7 +32,7 @@ import Usage from '$lib/components/Usage.svelte'; import { t } from '$lib/translations'; import { asyncSleep } from '$lib/common'; - import { appSession, search } from '$lib/store'; + import { appSession, search, addToast} from '$lib/store'; import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte'; import DatabaseIcons from '$lib/components/svg/databases/DatabaseIcons.svelte'; @@ -273,6 +273,7 @@ filtered = setInitials(); } } +
@@ -280,6 +281,7 @@ {#if $appSession.isAdmin && (applications.length !== 0 || destinations.length !== 0 || databases.length !== 0 || services.length !== 0 || gitSources.length !== 0 || destinations.length !== 0)} {/if} +