@@ -8,15 +8,15 @@
|
|||||||
"db:studio": "prisma studio",
|
"db:studio": "prisma studio",
|
||||||
"db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name",
|
"db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name",
|
||||||
"dev": "nodemon",
|
"dev": "nodemon",
|
||||||
"build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --platform=node --outdir=build --format=cjs",
|
"build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --minify=true --platform=node --outdir=build --format=cjs",
|
||||||
"format": "prettier --write 'src/**/*.{js,ts,json,md}'",
|
"format": "prettier --write 'src/**/*.{js,ts,json,md}'",
|
||||||
"lint": "prettier --check 'src/**/*.{js,ts,json,md}' && eslint --ignore-path .eslintignore .",
|
"lint": "prettier --check 'src/**/*.{js,ts,json,md}' && eslint --ignore-path .eslintignore .",
|
||||||
"start": "NODE_ENV=production npx -y prisma migrate deploy && npx prisma generate && npx prisma db seed && node index.js"
|
"start": "NODE_ENV=production npx -y prisma migrate deploy && npx prisma generate && npx prisma db seed && node index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@breejs/ts-worker": "2.0.0",
|
"@breejs/ts-worker": "2.0.0",
|
||||||
"@fastify/autoload": "5.3.1",
|
"@fastify/autoload": "5.4.0",
|
||||||
"@fastify/cookie": "8.1.0",
|
"@fastify/cookie": "8.3.0",
|
||||||
"@fastify/cors": "8.1.0",
|
"@fastify/cors": "8.1.0",
|
||||||
"@fastify/env": "4.1.0",
|
"@fastify/env": "4.1.0",
|
||||||
"@fastify/jwt": "6.3.2",
|
"@fastify/jwt": "6.3.2",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"@fastify/static": "6.5.0",
|
"@fastify/static": "6.5.0",
|
||||||
"@iarna/toml": "2.2.5",
|
"@iarna/toml": "2.2.5",
|
||||||
"@ladjs/graceful": "3.0.2",
|
"@ladjs/graceful": "3.0.2",
|
||||||
"@prisma/client": "4.3.1",
|
"@prisma/client": "4.4.0",
|
||||||
"axios": "0.27.2",
|
"axios": "0.27.2",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"bree": "9.1.2",
|
"bree": "9.1.2",
|
||||||
@@ -37,10 +37,10 @@
|
|||||||
"dockerode": "3.3.4",
|
"dockerode": "3.3.4",
|
||||||
"dotenv-extended": "2.9.0",
|
"dotenv-extended": "2.9.0",
|
||||||
"execa": "6.1.0",
|
"execa": "6.1.0",
|
||||||
"fastify": "4.5.3",
|
"fastify": "4.7.0",
|
||||||
"fastify-plugin": "4.2.1",
|
"fastify-plugin": "4.2.1",
|
||||||
"generate-password": "1.7.0",
|
"generate-password": "1.7.0",
|
||||||
"got": "12.4.1",
|
"got": "12.5.1",
|
||||||
"is-ip": "5.0.0",
|
"is-ip": "5.0.0",
|
||||||
"is-port-reachable": "4.0.0",
|
"is-port-reachable": "4.0.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
@@ -56,20 +56,20 @@
|
|||||||
"unique-names-generator": "4.7.1"
|
"unique-names-generator": "4.7.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "18.7.15",
|
"@types/node": "18.7.23",
|
||||||
"@types/node-os-utils": "1.3.0",
|
"@types/node-os-utils": "1.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "5.36.2",
|
"@typescript-eslint/eslint-plugin": "5.38.1",
|
||||||
"@typescript-eslint/parser": "5.36.2",
|
"@typescript-eslint/parser": "5.38.1",
|
||||||
"esbuild": "0.15.7",
|
"esbuild": "0.15.10",
|
||||||
"eslint": "8.23.0",
|
"eslint": "8.23.0",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-plugin-prettier": "4.2.1",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"nodemon": "2.0.19",
|
"nodemon": "2.0.20",
|
||||||
"prettier": "2.7.1",
|
"prettier": "2.7.1",
|
||||||
"prisma": "4.3.1",
|
"prisma": "4.4.0",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"tsconfig-paths": "4.1.0",
|
"tsconfig-paths": "4.1.0",
|
||||||
"typescript": "4.8.2"
|
"typescript": "4.8.4"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"seed": "node prisma/seed.js"
|
"seed": "node prisma/seed.js"
|
||||||
|
@@ -0,0 +1,2 @@
|
|||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "PreviewApplication_applicationId_key";
|
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Build" ADD COLUMN "sourceRepository" TEXT;
|
@@ -139,7 +139,7 @@ model PreviewApplication {
|
|||||||
sourceBranch String
|
sourceBranch String
|
||||||
isRandomDomain Boolean @default(false)
|
isRandomDomain Boolean @default(false)
|
||||||
customDomain String?
|
customDomain String?
|
||||||
applicationId String @unique
|
applicationId String
|
||||||
application Application @relation(fields: [applicationId], references: [id])
|
application Application @relation(fields: [applicationId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -247,6 +247,7 @@ model Build {
|
|||||||
previewApplicationId String?
|
previewApplicationId String?
|
||||||
forceRebuild Boolean @default(false)
|
forceRebuild Boolean @default(false)
|
||||||
sourceBranch String?
|
sourceBranch String?
|
||||||
|
sourceRepository String?
|
||||||
branch String?
|
branch String?
|
||||||
status String? @default("queued")
|
status String? @default("queued")
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
@@ -157,11 +157,6 @@ prisma.setting.findFirst().then(async (settings) => {
|
|||||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:copySSLCertificates")
|
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:copySSLCertificates")
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
|
||||||
// cleanupPrismaEngines
|
|
||||||
// setInterval(async () => {
|
|
||||||
// scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines")
|
|
||||||
// }, 60000)
|
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
getArch(),
|
getArch(),
|
||||||
getIPAddress(),
|
getIPAddress(),
|
||||||
|
@@ -38,7 +38,7 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
for (const queueBuild of queuedBuilds) {
|
for (const queueBuild of queuedBuilds) {
|
||||||
actions.push(async () => {
|
actions.push(async () => {
|
||||||
let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } })
|
let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } })
|
||||||
let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild } = queueBuild
|
let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild, sourceRepository = null } = queueBuild
|
||||||
application = decryptApplication(application)
|
application = decryptApplication(application)
|
||||||
const originalApplicationId = application.id
|
const originalApplicationId = application.id
|
||||||
if (pullmergeRequestId) {
|
if (pullmergeRequestId) {
|
||||||
@@ -54,7 +54,6 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
id: applicationId,
|
id: applicationId,
|
||||||
repository,
|
|
||||||
name,
|
name,
|
||||||
destinationDocker,
|
destinationDocker,
|
||||||
destinationDockerId,
|
destinationDockerId,
|
||||||
@@ -77,6 +76,7 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
} = application
|
} = application
|
||||||
let {
|
let {
|
||||||
branch,
|
branch,
|
||||||
|
repository,
|
||||||
buildPack,
|
buildPack,
|
||||||
port,
|
port,
|
||||||
installCommand,
|
installCommand,
|
||||||
@@ -135,6 +135,7 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
branch = sourceBranch;
|
branch = sourceBranch;
|
||||||
domain = `${pullmergeRequestId}.${domain}`;
|
domain = `${pullmergeRequestId}.${domain}`;
|
||||||
imageId = `${applicationId}-${pullmergeRequestId}`;
|
imageId = `${applicationId}-${pullmergeRequestId}`;
|
||||||
|
repository = sourceRepository || repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
let deployNeeded = true;
|
let deployNeeded = true;
|
||||||
|
@@ -146,12 +146,9 @@ async function checkProxies() {
|
|||||||
const { destinationDockerId, destinationDocker, publicPort, id } = database;
|
const { destinationDockerId, destinationDocker, publicPort, id } = database;
|
||||||
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
|
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
|
||||||
const { privatePort } = generateDatabaseConfiguration(database, arch);
|
const { privatePort } = generateDatabaseConfiguration(database, arch);
|
||||||
portReachable = await isReachable(publicPort, { host: destinationDocker.remoteIpAddress || ipv4 || ipv6 })
|
|
||||||
if (!portReachable) {
|
|
||||||
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
|
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const wordpressWithFtp = await prisma.wordpress.findMany({
|
const wordpressWithFtp = await prisma.wordpress.findMany({
|
||||||
where: { ftpPublicPort: { not: null } },
|
where: { ftpPublicPort: { not: null } },
|
||||||
include: { service: { include: { destinationDocker: true } } }
|
include: { service: { include: { destinationDocker: true } } }
|
||||||
@@ -160,12 +157,9 @@ async function checkProxies() {
|
|||||||
const { service, ftpPublicPort } = ftp;
|
const { service, ftpPublicPort } = ftp;
|
||||||
const { destinationDockerId, destinationDocker, id } = service;
|
const { destinationDockerId, destinationDocker, id } = service;
|
||||||
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
|
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
|
||||||
portReachable = await isReachable(ftpPublicPort, { host: destinationDocker.remoteIpAddress || ipv4 || ipv6 })
|
|
||||||
if (!portReachable) {
|
|
||||||
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
|
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP Proxies
|
// HTTP Proxies
|
||||||
const minioInstances = await prisma.minio.findMany({
|
const minioInstances = await prisma.minio.findMany({
|
||||||
@@ -176,12 +170,9 @@ async function checkProxies() {
|
|||||||
const { service, publicPort } = minio;
|
const { service, publicPort } = minio;
|
||||||
const { destinationDockerId, destinationDocker, id } = service;
|
const { destinationDockerId, destinationDocker, id } = service;
|
||||||
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
|
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
|
||||||
portReachable = await isReachable(publicPort, { host: destinationDocker.remoteIpAddress || ipv4 || ipv6 })
|
|
||||||
if (!portReachable) {
|
|
||||||
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
|
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,6 @@ import generator from 'generate-password';
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { promises as dns } from 'dns';
|
import { promises as dns } from 'dns';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import cuid from 'cuid';
|
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import sshConfig from 'ssh-config';
|
import sshConfig from 'ssh-config';
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ import { scheduler } from './scheduler';
|
|||||||
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
|
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
|
||||||
import { includeServices } from './services/common';
|
import { includeServices } from './services/common';
|
||||||
|
|
||||||
export const version = '3.10.12';
|
export const version = '3.10.13';
|
||||||
export const isDev = process.env.NODE_ENV === 'development';
|
export const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
const algorithm = 'aes-256-ctr';
|
const algorithm = 'aes-256-ctr';
|
||||||
@@ -45,7 +44,7 @@ export function getAPIUrl() {
|
|||||||
if (process.env.CODESANDBOX_HOST) {
|
if (process.env.CODESANDBOX_HOST) {
|
||||||
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
|
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
|
||||||
}
|
}
|
||||||
return isDev ? 'http://host.docker.internal:3001' : 'http://localhost:3000';
|
return isDev ? 'http://localhost:3001' : 'http://localhost:3000';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUIUrl() {
|
export function getUIUrl() {
|
||||||
@@ -1365,7 +1364,7 @@ export async function startTraefikTCPProxy(
|
|||||||
|
|
||||||
let dependentId = id;
|
let dependentId = id;
|
||||||
if (type === 'wordpressftp') dependentId = `${id}-ftp`;
|
if (type === 'wordpressftp') dependentId = `${id}-ftp`;
|
||||||
const foundDependentContainer = await checkContainer({
|
const { found: foundDependentContainer } = await checkContainer({
|
||||||
dockerId,
|
dockerId,
|
||||||
container: dependentId,
|
container: dependentId,
|
||||||
remove: true
|
remove: true
|
||||||
|
@@ -353,8 +353,10 @@ export async function getCurrentUser(
|
|||||||
// No new token -> not switching teams
|
// No new token -> not switching teams
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const pendingInvitations = await prisma.teamInvitation.findMany({ where: { uid: request.user.userId } })
|
||||||
return {
|
return {
|
||||||
settings: await prisma.setting.findFirst(),
|
settings: await prisma.setting.findFirst(),
|
||||||
|
pendingInvitations,
|
||||||
supportedServiceTypesAndVersions,
|
supportedServiceTypesAndVersions,
|
||||||
token,
|
token,
|
||||||
...request.user,
|
...request.user,
|
||||||
|
@@ -5,9 +5,10 @@ import { decrypt, errorHandler, prisma, uniqueName } from '../../../../lib/commo
|
|||||||
import { day } from '../../../../lib/dayjs';
|
import { day } from '../../../../lib/dayjs';
|
||||||
|
|
||||||
import type { OnlyId } from '../../../../types';
|
import type { OnlyId } from '../../../../types';
|
||||||
import type { BodyId, InviteToTeam, SaveTeam, SetPermission } from './types';
|
import type { BodyId, DeleteUserFromTeam, InviteToTeam, SaveTeam, SetPermission } from './types';
|
||||||
|
|
||||||
export async function listTeams(request: FastifyRequest) {
|
|
||||||
|
export async function listAccounts(request: FastifyRequest) {
|
||||||
try {
|
try {
|
||||||
const userId = request.user.userId;
|
const userId = request.user.userId;
|
||||||
const teamId = request.user.teamId;
|
const teamId = request.user.teamId;
|
||||||
@@ -15,10 +16,24 @@ export async function listTeams(request: FastifyRequest) {
|
|||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
select: { id: true, email: true, teams: true }
|
select: { id: true, email: true, teams: true }
|
||||||
});
|
});
|
||||||
let accounts = [];
|
let accounts = await prisma.user.findMany({ where: { teams: { some: { id: teamId } } }, select: { id: true, email: true, teams: true } });
|
||||||
let allTeams = [];
|
|
||||||
if (teamId === '0') {
|
if (teamId === '0') {
|
||||||
accounts = await prisma.user.findMany({ select: { id: true, email: true, teams: true } });
|
accounts = await prisma.user.findMany({ select: { id: true, email: true, teams: true } });
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
account,
|
||||||
|
accounts
|
||||||
|
};
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function listTeams(request: FastifyRequest) {
|
||||||
|
try {
|
||||||
|
const userId = request.user.userId;
|
||||||
|
const teamId = request.user.teamId;
|
||||||
|
let allTeams = [];
|
||||||
|
if (teamId === '0') {
|
||||||
allTeams = await prisma.team.findMany({
|
allTeams = await prisma.team.findMany({
|
||||||
where: { users: { none: { id: userId } } },
|
where: { users: { none: { id: userId } } },
|
||||||
include: { permissions: true }
|
include: { permissions: true }
|
||||||
@@ -28,18 +43,30 @@ export async function listTeams(request: FastifyRequest) {
|
|||||||
where: { users: { some: { id: userId } } },
|
where: { users: { some: { id: userId } } },
|
||||||
include: { permissions: true }
|
include: { permissions: true }
|
||||||
});
|
});
|
||||||
const invitations = await prisma.teamInvitation.findMany({ where: { uid: userId } });
|
|
||||||
return {
|
return {
|
||||||
ownTeams,
|
ownTeams,
|
||||||
allTeams,
|
allTeams,
|
||||||
invitations,
|
|
||||||
account,
|
|
||||||
accounts
|
|
||||||
};
|
};
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export async function removeUserFromTeam(request: FastifyRequest<DeleteUserFromTeam>, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const { uid } = request.body;
|
||||||
|
const { id } = request.params;
|
||||||
|
const userId = request.user.userId;
|
||||||
|
const foundUser = await prisma.team.findMany({ where: { id, users: { some: { id: userId } } } });
|
||||||
|
if (foundUser.length === 0) {
|
||||||
|
return errorHandler({ status: 404, message: 'Team not found' });
|
||||||
|
}
|
||||||
|
await prisma.team.update({ where: { id }, data: { users: { disconnect: { id: uid } } } });
|
||||||
|
await prisma.permission.deleteMany({ where: { teamId: id, userId: uid } })
|
||||||
|
return reply.code(201).send()
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
export async function deleteTeam(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
|
export async function deleteTeam(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const userId = request.user.userId;
|
const userId = request.user.userId;
|
||||||
|
@@ -1,19 +1,22 @@
|
|||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import { acceptInvitation, changePassword, deleteTeam, getTeam, inviteToTeam, listTeams, newTeam, removeUser, revokeInvitation, saveTeam, setPermission } from './handlers';
|
import { acceptInvitation, changePassword, deleteTeam, getTeam, inviteToTeam, listAccounts, listTeams, newTeam, removeUser, removeUserFromTeam, revokeInvitation, saveTeam, setPermission } from './handlers';
|
||||||
|
|
||||||
import type { OnlyId } from '../../../../types';
|
import type { OnlyId } from '../../../../types';
|
||||||
import type { BodyId, InviteToTeam, SaveTeam, SetPermission } from './types';
|
import type { BodyId, DeleteUserFromTeam, InviteToTeam, SaveTeam, SetPermission } from './types';
|
||||||
|
|
||||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||||
fastify.addHook('onRequest', async (request) => {
|
fastify.addHook('onRequest', async (request) => {
|
||||||
return await request.jwtVerify()
|
return await request.jwtVerify()
|
||||||
})
|
})
|
||||||
fastify.get('/', async (request) => await listTeams(request));
|
|
||||||
|
fastify.get('/', async (request) => await listAccounts(request));
|
||||||
fastify.post('/new', async (request, reply) => await newTeam(request, reply));
|
fastify.post('/new', async (request, reply) => await newTeam(request, reply));
|
||||||
|
fastify.get('/teams', async (request) => await listTeams(request));
|
||||||
|
|
||||||
fastify.get<OnlyId>('/team/:id', async (request, reply) => await getTeam(request, reply));
|
fastify.get<OnlyId>('/team/:id', async (request, reply) => await getTeam(request, reply));
|
||||||
fastify.post<SaveTeam>('/team/:id', async (request, reply) => await saveTeam(request, reply));
|
fastify.post<SaveTeam>('/team/:id', async (request, reply) => await saveTeam(request, reply));
|
||||||
fastify.delete<OnlyId>('/team/:id', async (request, reply) => await deleteTeam(request, reply));
|
fastify.delete<OnlyId>('/team/:id', async (request, reply) => await deleteTeam(request, reply));
|
||||||
|
fastify.post<DeleteUserFromTeam>('/team/:id/user/remove', async (request, reply) => await removeUserFromTeam(request, reply));
|
||||||
|
|
||||||
fastify.post<InviteToTeam>('/team/:id/invitation/invite', async (request, reply) => await inviteToTeam(request, reply))
|
fastify.post<InviteToTeam>('/team/:id/invitation/invite', async (request, reply) => await inviteToTeam(request, reply))
|
||||||
fastify.post<BodyId>('/team/:id/invitation/accept', async (request) => await acceptInvitation(request));
|
fastify.post<BodyId>('/team/:id/invitation/accept', async (request) => await acceptInvitation(request));
|
||||||
@@ -23,7 +26,6 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
|
|
||||||
fastify.delete<BodyId>('/user/remove', async (request, reply) => await removeUser(request, reply));
|
fastify.delete<BodyId>('/user/remove', async (request, reply) => await removeUser(request, reply));
|
||||||
fastify.post<BodyId>('/user/password', async (request, reply) => await changePassword(request, reply));
|
fastify.post<BodyId>('/user/password', async (request, reply) => await changePassword(request, reply));
|
||||||
// fastify.delete('/user', async (request, reply) => await deleteUser(request, reply));
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -5,6 +5,14 @@ export interface SaveTeam extends OnlyId {
|
|||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export interface DeleteUserFromTeam {
|
||||||
|
Body: {
|
||||||
|
uid: string
|
||||||
|
},
|
||||||
|
Params: {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
}
|
||||||
export interface InviteToTeam {
|
export interface InviteToTeam {
|
||||||
Body: {
|
Body: {
|
||||||
email: string,
|
email: string,
|
||||||
|
@@ -66,13 +66,19 @@ export async function configureGitHubApp(request, reply) {
|
|||||||
}
|
}
|
||||||
export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promise<any> {
|
export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const allowedGithubEvents = ['push', 'pull_request'];
|
const allowedGithubEvents = ['push', 'pull_request', 'ping', 'installation'];
|
||||||
const allowedActions = ['opened', 'reopened', 'synchronize', 'closed'];
|
const allowedActions = ['opened', 'reopened', 'synchronize', 'closed'];
|
||||||
const githubEvent = request.headers['x-github-event']?.toString().toLowerCase();
|
const githubEvent = request.headers['x-github-event']?.toString().toLowerCase();
|
||||||
const githubSignature = request.headers['x-hub-signature-256']?.toString().toLowerCase();
|
const githubSignature = request.headers['x-hub-signature-256']?.toString().toLowerCase();
|
||||||
if (!allowedGithubEvents.includes(githubEvent)) {
|
if (!allowedGithubEvents.includes(githubEvent)) {
|
||||||
throw { status: 500, message: 'Event not allowed.' }
|
throw { status: 500, message: 'Event not allowed.' }
|
||||||
}
|
}
|
||||||
|
if (githubEvent === 'ping') {
|
||||||
|
return { pong: 'cool' }
|
||||||
|
}
|
||||||
|
if (githubEvent === 'installation') {
|
||||||
|
return { status: 'cool' }
|
||||||
|
}
|
||||||
let projectId, branch;
|
let projectId, branch;
|
||||||
const body = request.body
|
const body = request.body
|
||||||
if (githubEvent === 'push') {
|
if (githubEvent === 'push') {
|
||||||
@@ -80,7 +86,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
|||||||
branch = body.ref.includes('/') ? body.ref.split('/')[2] : body.ref;
|
branch = body.ref.includes('/') ? body.ref.split('/')[2] : body.ref;
|
||||||
} else if (githubEvent === 'pull_request') {
|
} else if (githubEvent === 'pull_request') {
|
||||||
projectId = body.pull_request.base.repo.id;
|
projectId = body.pull_request.base.repo.id;
|
||||||
branch = body.pull_request.base.ref.includes('/') ? body.pull_request.base.ref.split('/')[2] : body.pull_request.base.ref;
|
branch = body.pull_request.base.ref
|
||||||
}
|
}
|
||||||
if (!projectId || !branch) {
|
if (!projectId || !branch) {
|
||||||
throw { status: 500, message: 'Cannot parse projectId or branch from the webhook?!' }
|
throw { status: 500, message: 'Cannot parse projectId or branch from the webhook?!' }
|
||||||
@@ -147,7 +153,8 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
|||||||
} else if (githubEvent === 'pull_request') {
|
} else if (githubEvent === 'pull_request') {
|
||||||
const pullmergeRequestId = body.number.toString();
|
const pullmergeRequestId = body.number.toString();
|
||||||
const pullmergeRequestAction = body.action;
|
const pullmergeRequestAction = body.action;
|
||||||
const sourceBranch = body.pull_request.head.ref.includes('/') ? body.pull_request.head.ref.split('/')[2] : body.pull_request.head.ref;
|
const sourceBranch = body.pull_request.head.ref
|
||||||
|
const sourceRepository = body.pull_request.head.repo.full_name
|
||||||
if (!allowedActions.includes(pullmergeRequestAction)) {
|
if (!allowedActions.includes(pullmergeRequestAction)) {
|
||||||
throw { status: 500, message: 'Action not allowed.' }
|
throw { status: 500, message: 'Action not allowed.' }
|
||||||
}
|
}
|
||||||
@@ -205,6 +212,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
|||||||
await prisma.build.create({
|
await prisma.build.create({
|
||||||
data: {
|
data: {
|
||||||
id: buildId,
|
id: buildId,
|
||||||
|
sourceRepository,
|
||||||
pullmergeRequestId,
|
pullmergeRequestId,
|
||||||
previewApplicationId,
|
previewApplicationId,
|
||||||
sourceBranch,
|
sourceBranch,
|
||||||
|
@@ -23,6 +23,7 @@ export interface GitHubEvents {
|
|||||||
ref: string,
|
ref: string,
|
||||||
repo: {
|
repo: {
|
||||||
id: string,
|
id: string,
|
||||||
|
full_name: string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -39,9 +39,7 @@ export async function configureGitLabApp(request: FastifyRequest<ConfigureGitLab
|
|||||||
export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
||||||
const { object_kind: objectKind, ref, project_id } = request.body
|
const { object_kind: objectKind, ref, project_id } = request.body
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const allowedActions = ['opened', 'reopen', 'close', 'open', 'update'];
|
const allowedActions = ['opened', 'reopen', 'close', 'open', 'update'];
|
||||||
|
|
||||||
const webhookToken = request.headers['x-gitlab-token'];
|
const webhookToken = request.headers['x-gitlab-token'];
|
||||||
if (!webhookToken && !isDev) {
|
if (!webhookToken && !isDev) {
|
||||||
throw { status: 500, message: 'Invalid webhookToken.' }
|
throw { status: 500, message: 'Invalid webhookToken.' }
|
||||||
@@ -91,7 +89,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (objectKind === 'merge_request') {
|
} else if (objectKind === 'merge_request') {
|
||||||
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch }, project: { id } } = request.body
|
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch, source: { path_with_namespace: sourceRepository } }, project: { id } } = request.body
|
||||||
const pullmergeRequestId = request.body.object_attributes.iid.toString();
|
const pullmergeRequestId = request.body.object_attributes.iid.toString();
|
||||||
const projectId = Number(id);
|
const projectId = Number(id);
|
||||||
if (!allowedActions.includes(action)) {
|
if (!allowedActions.includes(action)) {
|
||||||
@@ -100,7 +98,6 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
|||||||
if (isDraft) {
|
if (isDraft) {
|
||||||
throw { status: 500, message: 'Draft MR, do nothing.' }
|
throw { status: 500, message: 'Draft MR, do nothing.' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const applicationsFound = await getApplicationFromDBWebhook(projectId, targetBranch);
|
const applicationsFound = await getApplicationFromDBWebhook(projectId, targetBranch);
|
||||||
if (applicationsFound && applicationsFound.length > 0) {
|
if (applicationsFound && applicationsFound.length > 0) {
|
||||||
for (const application of applicationsFound) {
|
for (const application of applicationsFound) {
|
||||||
@@ -153,6 +150,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
|||||||
id: buildId,
|
id: buildId,
|
||||||
pullmergeRequestId,
|
pullmergeRequestId,
|
||||||
previewApplicationId,
|
previewApplicationId,
|
||||||
|
sourceRepository,
|
||||||
sourceBranch,
|
sourceBranch,
|
||||||
applicationId: application.id,
|
applicationId: application.id,
|
||||||
destinationDockerId: application.destinationDocker.id,
|
destinationDockerId: application.destinationDocker.id,
|
||||||
|
@@ -8,6 +8,9 @@ export interface GitLabEvents {
|
|||||||
Body: {
|
Body: {
|
||||||
object_attributes: {
|
object_attributes: {
|
||||||
work_in_progress: string
|
work_in_progress: string
|
||||||
|
source: {
|
||||||
|
path_with_namespace: string
|
||||||
|
}
|
||||||
isDraft: string
|
isDraft: string
|
||||||
action: string
|
action: string
|
||||||
source_branch: string
|
source_branch: string
|
||||||
|
@@ -328,7 +328,7 @@
|
|||||||
"members": "Members",
|
"members": "Members",
|
||||||
"root_team_explainer": "This is the <span class='text-red-500 '>root</span> team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux).",
|
"root_team_explainer": "This is the <span class='text-red-500 '>root</span> team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux).",
|
||||||
"permission": "Permission",
|
"permission": "Permission",
|
||||||
"you": "(You)",
|
"you": "You",
|
||||||
"promote_to": "Promote to {{grade}}",
|
"promote_to": "Promote to {{grade}}",
|
||||||
"revoke_invitation": "Revoke invitation",
|
"revoke_invitation": "Revoke invitation",
|
||||||
"pending_invitation": "Pending invitation",
|
"pending_invitation": "Pending invitation",
|
||||||
|
@@ -318,6 +318,6 @@
|
|||||||
"root": "(suprême)",
|
"root": "(suprême)",
|
||||||
"root_team_explainer": "Il s'agit de l'équipe <span class='text-red-500 font-bold'>suprême</span>. \nCela signifie que les membres de ce groupe peuvent gérer les paramètres à l'échelle de l'instance et avoir tous les privilèges dans Coolify (imaginez comme un utilisateur root sous Linux).",
|
"root_team_explainer": "Il s'agit de l'équipe <span class='text-red-500 font-bold'>suprême</span>. \nCela signifie que les membres de ce groupe peuvent gérer les paramètres à l'échelle de l'instance et avoir tous les privilèges dans Coolify (imaginez comme un utilisateur root sous Linux).",
|
||||||
"send_invitation": "Envoyer une invitation",
|
"send_invitation": "Envoyer une invitation",
|
||||||
"you": "(Toi)"
|
"you": "Toi"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,7 @@ interface AppSession {
|
|||||||
gitlab: string | null,
|
gitlab: string | null,
|
||||||
},
|
},
|
||||||
supportedServiceTypesAndVersions: Array<any>
|
supportedServiceTypesAndVersions: Array<any>
|
||||||
|
pendingInvitations: Array<any>
|
||||||
}
|
}
|
||||||
interface AddToast {
|
interface AddToast {
|
||||||
type?: "info" | "success" | "error",
|
type?: "info" | "success" | "error",
|
||||||
@@ -47,7 +48,8 @@ export const appSession: Writable<AppSession> = writable({
|
|||||||
github: null,
|
github: null,
|
||||||
gitlab: null
|
gitlab: null
|
||||||
},
|
},
|
||||||
supportedServiceTypesAndVersions: []
|
supportedServiceTypesAndVersions: [],
|
||||||
|
pendingInvitations: []
|
||||||
});
|
});
|
||||||
export const disabledButton: Writable<boolean> = writable(false);
|
export const disabledButton: Writable<boolean> = writable(false);
|
||||||
export const isDeploymentEnabled: Writable<boolean> = writable(false);
|
export const isDeploymentEnabled: Writable<boolean> = writable(false);
|
||||||
|
@@ -66,6 +66,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let baseSettings: any;
|
export let baseSettings: any;
|
||||||
export let supportedServiceTypesAndVersions: any;
|
export let supportedServiceTypesAndVersions: any;
|
||||||
|
export let pendingInvitations: any = 0;
|
||||||
|
|
||||||
$appSession.isRegistrationEnabled = baseSettings.isRegistrationEnabled;
|
$appSession.isRegistrationEnabled = baseSettings.isRegistrationEnabled;
|
||||||
$appSession.ipv4 = baseSettings.ipv4;
|
$appSession.ipv4 = baseSettings.ipv4;
|
||||||
$appSession.ipv6 = baseSettings.ipv6;
|
$appSession.ipv6 = baseSettings.ipv6;
|
||||||
@@ -74,10 +76,13 @@
|
|||||||
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
|
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
|
||||||
$appSession.supportedServiceTypesAndVersions = supportedServiceTypesAndVersions;
|
$appSession.supportedServiceTypesAndVersions = supportedServiceTypesAndVersions;
|
||||||
|
|
||||||
|
$appSession.pendingInvitations = pendingInvitations;
|
||||||
|
|
||||||
export let userId: string;
|
export let userId: string;
|
||||||
export let teamId: string;
|
export let teamId: string;
|
||||||
export let permission: string;
|
export let permission: string;
|
||||||
export let isAdmin: boolean;
|
export let isAdmin: boolean;
|
||||||
|
|
||||||
import '../tailwind.css';
|
import '../tailwind.css';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
@@ -202,11 +207,16 @@
|
|||||||
<a
|
<a
|
||||||
id="iam"
|
id="iam"
|
||||||
sveltekit:prefetch
|
sveltekit:prefetch
|
||||||
href="/iam"
|
href={$appSession.pendingInvitations.length > 0 ? '/iam/pending' : '/iam'}
|
||||||
class="icons hover:text-iam"
|
class="icons hover:text-iam indicator"
|
||||||
class:text-iam={$page.url.pathname.startsWith('/iam')}
|
class:text-iam={$page.url.pathname.startsWith('/iam')}
|
||||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
|
class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
|
||||||
><svg
|
>
|
||||||
|
{#if $appSession.pendingInvitations.length > 0}
|
||||||
|
<span class="indicator-item rounded-full badge badge-primary mr-2"
|
||||||
|
>{pendingInvitations.length}</span
|
||||||
|
>
|
||||||
|
{/if}<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
class="h-9 w-9"
|
class="h-9 w-9"
|
||||||
@@ -342,6 +352,7 @@
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
|
id="servers"
|
||||||
class="no-underline icons hover:text-white hover:bg-sky-500"
|
class="no-underline icons hover:text-white hover:bg-sky-500"
|
||||||
sveltekit:prefetch
|
sveltekit:prefetch
|
||||||
href="/servers"
|
href="/servers"
|
||||||
@@ -387,7 +398,11 @@
|
|||||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||||
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
|
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
|
||||||
</svg>
|
</svg>
|
||||||
IAM
|
IAM {#if $appSession.pendingInvitations.length > 0}
|
||||||
|
<span class="indicator-item rounded-full badge badge-primary"
|
||||||
|
>{pendingInvitations.length}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@@ -184,10 +184,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if application.previewApplication.length > 0}
|
{:else if application.previewApplication.length > 0}
|
||||||
<div
|
<div
|
||||||
class="grid grid-col gap-4 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
class="grid grid-col gap-4 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-2 px-6"
|
||||||
>
|
>
|
||||||
{#each application.previewApplication as preview}
|
{#each application.previewApplication as preview}
|
||||||
<div class="no-underline mb-5 w-full lg:w-96">
|
<div class="no-underline mb-5 w-full">
|
||||||
<div class="w-full rounded p-5 bg-coolgray-200 indicator">
|
<div class="w-full rounded p-5 bg-coolgray-200 indicator">
|
||||||
{#await getStatus(preview)}
|
{#await getStatus(preview)}
|
||||||
<span class="indicator-item badge bg-yellow-500 badge-sm" />
|
<span class="indicator-item badge bg-yellow-500 badge-sm" />
|
||||||
|
64
apps/ui/src/routes/iam/_Account.svelte
Normal file
64
apps/ui/src/routes/iam/_Account.svelte
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let account: any;
|
||||||
|
export let accounts: any = [];
|
||||||
|
import { del, get, post } from '$lib/api';
|
||||||
|
import { errorNotification } from '$lib/common';
|
||||||
|
import { addToast, appSession } from '$lib/store';
|
||||||
|
async function resetPassword(id: any) {
|
||||||
|
const sure = window.confirm('Are you sure you want to reset the password?');
|
||||||
|
if (!sure) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await post(`/iam/user/password`, { id });
|
||||||
|
return addToast({
|
||||||
|
message: 'Password reset successfully. Please relogin to reset it.',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return errorNotification(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function deleteAccount(id: any) {
|
||||||
|
if (id === $appSession.userId || account.id === '0') return;
|
||||||
|
const sure = window.confirm('Are you sure you want to delete this user?');
|
||||||
|
if (!sure) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await del(`/iam/user/remove`, { id });
|
||||||
|
addToast({
|
||||||
|
message: 'Account deleted.',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
const data = await get('/iam');
|
||||||
|
accounts = data.accounts;
|
||||||
|
} catch (error) {
|
||||||
|
return errorNotification(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col lg:flex-row lg:space-y-0 space-y-2 lg:space-x-4">
|
||||||
|
<input
|
||||||
|
disabled
|
||||||
|
class="input w-full text-white"
|
||||||
|
readonly
|
||||||
|
placeholder="email"
|
||||||
|
value={account.email}
|
||||||
|
/>
|
||||||
|
<div class="flex flex-row items-center justify-center space-x-2 w-full lg:w-96">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<button class="btn btn-sm btn-primary" on:click={() => resetPassword(account.id)}
|
||||||
|
>Reset Password</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-error"
|
||||||
|
disabled={account.id === $appSession.userId || account.id === '0'}
|
||||||
|
on:click={() => deleteAccount(account.id)}>Delete Account</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
73
apps/ui/src/routes/iam/_Menu.svelte
Normal file
73
apps/ui/src/routes/iam/_Menu.svelte
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { appSession } from '$lib/store';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4">
|
||||||
|
{#if $appSession.pendingInvitations.length > 0}
|
||||||
|
<li class="menu-title">
|
||||||
|
<span>IAM</span>
|
||||||
|
</li>
|
||||||
|
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/iam/pending`}>
|
||||||
|
<a href={`/iam/pending`} class="no-underline w-full"
|
||||||
|
><svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M10 21v-6.5a3.5 3.5 0 0 0 -7 0v6.5h18v-6a4 4 0 0 0 -4 -4h-10.5" />
|
||||||
|
<path d="M12 11v-8h4l2 2l-2 2h-4" />
|
||||||
|
<path d="M6 15h1" />
|
||||||
|
</svg>Pending Invitations</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
<li class="menu-title">
|
||||||
|
<span>IAM</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/iam`}>
|
||||||
|
<a href={`/iam`} class="no-underline w-full"
|
||||||
|
><svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<circle cx="12" cy="7" r="4" />
|
||||||
|
<path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
|
||||||
|
</svg>{$appSession.userId === '0' && $appSession.teamId === '0' ? 'Accounts' : 'Account'}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="rounded" class:bg-coollabs={$page.url.pathname.startsWith(`/iam/teams`)}>
|
||||||
|
<a href={`/iam/teams`} class="no-underline w-full"
|
||||||
|
><svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<circle cx="9" cy="7" r="4" />
|
||||||
|
<path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||||
|
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
|
||||||
|
</svg>Teams</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
20
apps/ui/src/routes/iam/__layout.svelte
Normal file
20
apps/ui/src/routes/iam/__layout.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { appSession } from '$lib/store';
|
||||||
|
import Menu from './_Menu.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-screen-2xl px-6 grid grid-cols-1 lg:grid-cols-2">
|
||||||
|
<nav class="header flex flex-row order-2 lg:order-1 px-0 lg:px-4 items-start">
|
||||||
|
<div class="title lg:pb-10">Identity & Access Management</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto max-w-screen-2xl px-0 lg:px-2 grid grid-cols-1 lg:grid-cols-4">
|
||||||
|
<nav class="header flex flex-col lg:pt-0 lg:col-span-1">
|
||||||
|
<Menu />
|
||||||
|
</nav>
|
||||||
|
<div class="pt-0 lg:col-span-3 pb-24 px-6">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -20,224 +20,54 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let account: any;
|
export let account: any;
|
||||||
export let accounts: any;
|
export let accounts: any;
|
||||||
export let invitations: any;
|
|
||||||
export let ownTeams: any;
|
|
||||||
export let allTeams: any;
|
|
||||||
|
|
||||||
import { del, get, post } from '$lib/api';
|
import { appSession } from '$lib/store';
|
||||||
import { errorNotification } from '$lib/common';
|
import { get, post } from '$lib/api';
|
||||||
import { addToast, appSession } from '$lib/store';
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import Cookies from 'js-cookie';
|
import { page } from '$app/stores';
|
||||||
if (accounts.length === 0) {
|
import Account from './_Account.svelte';
|
||||||
accounts.push(account);
|
let search = '';
|
||||||
}
|
let searchResults: any = [];
|
||||||
|
|
||||||
async function resetPassword(id: any) {
|
function searchAccount() {
|
||||||
const sure = window.confirm('Are you sure you want to reset the password?');
|
searchResults = accounts.filter((account: { email: string | string[] }) => {
|
||||||
if (!sure) {
|
return account.email.includes(search);
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await post(`/iam/user/password`, { id });
|
|
||||||
return addToast({
|
|
||||||
message: 'Password reset successfully. Please relogin to reset it.',
|
|
||||||
type: 'success'
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
return errorNotification(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function deleteUser(id: any) {
|
|
||||||
const sure = window.confirm('Are you sure you want to delete this user?');
|
|
||||||
if (!sure) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await del(`/iam/user/remove`, { id });
|
|
||||||
addToast({
|
|
||||||
message: 'Account deleted.',
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
const data = await get('/iam');
|
|
||||||
accounts = data.accounts;
|
|
||||||
} catch (error) {
|
|
||||||
return errorNotification(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function acceptInvitation(id: any, teamId: any) {
|
|
||||||
try {
|
|
||||||
await post(`/iam/team/${teamId}/invitation/accept`, { id });
|
|
||||||
return window.location.reload();
|
|
||||||
} catch (error) {
|
|
||||||
return errorNotification(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function revokeInvitation(id: any, teamId: any) {
|
|
||||||
try {
|
|
||||||
await post(`/iam/team/${teamId}/invitation/revoke`, { id });
|
|
||||||
return window.location.reload();
|
|
||||||
} catch (error) {
|
|
||||||
return errorNotification(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function switchTeam(selectedTeamId: any) {
|
|
||||||
try {
|
|
||||||
const payload = await get(`/user?teamId=${selectedTeamId}`);
|
|
||||||
if (payload.token) {
|
|
||||||
Cookies.set('token', payload.token, {
|
|
||||||
path: '/'
|
|
||||||
});
|
|
||||||
$appSession.teamId = payload.teamId;
|
|
||||||
$appSession.userId = payload.userId;
|
|
||||||
$appSession.permission = payload.permission;
|
|
||||||
$appSession.isAdmin = payload.isAdmin;
|
|
||||||
return window.location.reload();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
return errorNotification(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function newTeam() {
|
|
||||||
const { id } = await post('/iam/new', {});
|
|
||||||
return await goto(`/iam/team/${id}`, { replaceState: true });
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="header">
|
<div class="w-full">
|
||||||
<h1 class="mr-4 text-2xl tracking-tight font-bold">Identity and Access Management</h1>
|
<div class="mx-auto w-full">
|
||||||
<button on:click={newTeam} class="btn btn-square btn-sm bg-iam">
|
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 items-center">
|
||||||
<svg
|
<div class="title font-bold pb-3">
|
||||||
class="h-6 w-6"
|
{$appSession.userId === '0' && $appSession.teamId === '0' ? 'Accounts' : 'Your account'}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
</div>
|
||||||
fill="none"
|
</div>
|
||||||
viewBox="0 0 24 24"
|
</div>
|
||||||
stroke="currentColor"
|
</div>
|
||||||
><path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
{#if invitations.length > 0}
|
{#if $appSession.userId === '0' && $appSession.teamId === '0'}
|
||||||
<div class="mx-auto max-w-6xl px-6 py-4">
|
<div class="w-full grid gap-2">
|
||||||
<div class="title font-bold">Pending invitations</div>
|
<input
|
||||||
<div class="pt-10 text-center">
|
class="input w-full mb-4"
|
||||||
{#each invitations as invitation}
|
bind:value={search}
|
||||||
<div class="flex justify-center space-x-2">
|
on:input={searchAccount}
|
||||||
<div>
|
placeholder="Search for account..."
|
||||||
Invited to <span class="font-bold text-pink-600">{invitation.teamName}</span> with
|
/>
|
||||||
<span class="font-bold text-rose-600">{invitation.permission}</span> permission.
|
<div class="flex flex-col pb-2 space-y-4 lg:space-y-2">
|
||||||
</div>
|
{#if searchResults.length > 0}
|
||||||
<button
|
{#each searchResults as account}
|
||||||
class="btn btn-sm btn-success"
|
<Account {account} {accounts} />
|
||||||
on:click={() => acceptInvitation(invitation.id, invitation.teamId)}>Accept</button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-error"
|
|
||||||
on:click={() => revokeInvitation(invitation.id, invitation.teamId)}>Delete</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
{:else if searchResults.length === 0 && search !== ''}
|
||||||
</div>
|
<div>Nothing found.</div>
|
||||||
{/if}
|
|
||||||
<div class="mx-auto max-w-6xl px-6 py-4">
|
|
||||||
{#if $appSession.teamId === '0' && accounts.length > 0}
|
|
||||||
<div class="title font-bold">Accounts</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="title font-bold">Account</div>
|
|
||||||
{/if}
|
|
||||||
<div class="flex items-center justify-center pt-10">
|
|
||||||
<table class="mx-2 text-left">
|
|
||||||
<thead class="mb-2">
|
|
||||||
<tr>
|
|
||||||
{#if accounts.length > 1}
|
|
||||||
<th class="px-2">Email</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
{/if}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{#each accounts as account}
|
{#each accounts as account}
|
||||||
<tr class="grid items-center justify-center gap-2 lg:grid-flow-col">
|
<Account {account} {accounts} />
|
||||||
<td class="px-2">{account.email}</td>
|
|
||||||
<td class="flex space-x-2">
|
|
||||||
<form on:submit|preventDefault={() => resetPassword(account.id)}>
|
|
||||||
<button class="my-4 btn btn-sm bg-iam">Reset Password</button>
|
|
||||||
</form>
|
|
||||||
<form on:submit|preventDefault={() => deleteUser(account.id)}>
|
|
||||||
<button
|
|
||||||
disabled={account.id === $appSession.userId}
|
|
||||||
class="my-4 btn btn-sm"
|
|
||||||
type="submit">Delete User</button
|
|
||||||
>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mx-auto max-w-6xl px-6">
|
|
||||||
<div class="title font-bold">Teams</div>
|
|
||||||
<div class="flex-col items-center justify-center pt-10">
|
|
||||||
<div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row">
|
|
||||||
{#each ownTeams as team}
|
|
||||||
<a href="/iam/team/{team.id}" class="p-2 no-underline">
|
|
||||||
<div class="box-selection relative">
|
|
||||||
<div>
|
|
||||||
<div class="truncate text-center text-xl font-bold">
|
|
||||||
{team.name}
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 text-center text-xs">
|
|
||||||
{team.permissions?.length} member(s)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-center pt-3">
|
|
||||||
<button
|
|
||||||
on:click|preventDefault={() => switchTeam(team.id)}
|
|
||||||
class="btn btn-sm"
|
|
||||||
class:bg-fuchsia-600={$appSession.teamId !== team.id}
|
|
||||||
class:hover:bg-fuchsia-500={$appSession.teamId !== team.id}
|
|
||||||
class:bg-transparent={$appSession.teamId === team.id}
|
|
||||||
disabled={$appSession.teamId === team.id}
|
|
||||||
>{$appSession.teamId === team.id ? 'Current Team' : 'Switch Team'}</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{#if $appSession.teamId === '0' && allTeams.length > 0}
|
|
||||||
<div class="pb-5 pt-10 text-xl font-bold">Other Teams</div>
|
|
||||||
<div class="flex flex-row flex-wrap justify-center px-2 md:flex-row">
|
|
||||||
{#each allTeams as team}
|
|
||||||
<a href="/iam/team/{team.id}" class="p-2 no-underline">
|
|
||||||
<div
|
|
||||||
class="box-selection relative"
|
|
||||||
class:hover:bg-fuchsia-600={team.id !== '0'}
|
|
||||||
class:hover:bg-red-500={team.id === '0'}
|
|
||||||
>
|
|
||||||
<div class="truncate text-center text-xl font-bold">
|
|
||||||
{team.name}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-1 text-center">{team.permissions?.length} member(s)</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Account {account} />
|
||||||
|
{/if}
|
||||||
|
55
apps/ui/src/routes/iam/pending.svelte
Normal file
55
apps/ui/src/routes/iam/pending.svelte
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { post } from '$lib/api';
|
||||||
|
import { errorNotification } from '$lib/common';
|
||||||
|
import { appSession } from '$lib/store';
|
||||||
|
if ($appSession.pendingInvitations.length === 0) {
|
||||||
|
goto('/iam/teams');
|
||||||
|
}
|
||||||
|
async function acceptInvitation(id: any, teamId: any) {
|
||||||
|
try {
|
||||||
|
await post(`/iam/team/${teamId}/invitation/accept`, { id });
|
||||||
|
return window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
return errorNotification(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function revokeInvitation(id: any, teamId: any) {
|
||||||
|
try {
|
||||||
|
await post(`/iam/team/${teamId}/invitation/revoke`, { id });
|
||||||
|
return window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
return errorNotification(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="mx-auto w-full">
|
||||||
|
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 items-center">
|
||||||
|
<div class="title font-bold pb-3">Pending Invitations</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full grid gap-2">
|
||||||
|
<div class="flex flex-col pb-2 space-y-4 lg:space-y-2">
|
||||||
|
{#each $appSession.pendingInvitations as invitation}
|
||||||
|
<div class="flex flex-col justify-center items-center">
|
||||||
|
<div class="text-xl pb-4 text-center">
|
||||||
|
Invited to <span class="font-bold text-pink-500">{invitation.teamName}</span> with
|
||||||
|
<span class="font-bold text-red-500">{invitation.permission}</span> permission.
|
||||||
|
</div>
|
||||||
|
<div class=" flex space-x-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
on:click={() => acceptInvitation(invitation.id, invitation.teamId)}>Accept</button
|
||||||
|
>
|
||||||
|
<button class="btn" on:click={() => revokeInvitation(invitation.id, invitation.teamId)}
|
||||||
|
>Ignore</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -1,84 +0,0 @@
|
|||||||
<script context="module" lang="ts">
|
|
||||||
import { del, get } from '$lib/api';
|
|
||||||
import type { Load } from '@sveltejs/kit';
|
|
||||||
export const load: Load = async ({ params, url }) => {
|
|
||||||
try {
|
|
||||||
const response = await get(`/iam/team/${params.id}`);
|
|
||||||
if (!response.permissions || Object.entries(response.permissions).length === 0) {
|
|
||||||
return {
|
|
||||||
status: 302,
|
|
||||||
redirect: '/iam'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
...response
|
|
||||||
},
|
|
||||||
stuff: {
|
|
||||||
...response
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return handlerNotFoundLoad(error, url);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export let team: any;
|
|
||||||
export let currentTeam: string;
|
|
||||||
export let teams: any;
|
|
||||||
import { page } from '$app/stores';
|
|
||||||
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
|
|
||||||
import { appSession } from '$lib/store';
|
|
||||||
import { t } from '$lib/translations';
|
|
||||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
|
||||||
const { id } = $page.params;
|
|
||||||
|
|
||||||
async function deleteTeam() {
|
|
||||||
const sure = confirm('Are you sure you want to delete this team?');
|
|
||||||
if (sure) {
|
|
||||||
try {
|
|
||||||
await del(`/iam/team/${id}`, { id });
|
|
||||||
if (currentTeam === id) {
|
|
||||||
const switchTeam = teams.find((team: any) => team.id !== id);
|
|
||||||
const payload = await get(`/user?teamId=${switchTeam.id}`);
|
|
||||||
if (payload.token) {
|
|
||||||
Cookies.set('token', payload.token, {
|
|
||||||
path: '/'
|
|
||||||
});
|
|
||||||
$appSession.teamId = payload.teamId;
|
|
||||||
$appSession.userId = payload.userId;
|
|
||||||
$appSession.permission = payload.permission;
|
|
||||||
$appSession.isAdmin = payload.isAdmin;
|
|
||||||
return window.location.assign('/iam');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await goto('/iam', { replaceState: true });
|
|
||||||
} catch (error) {
|
|
||||||
return errorNotification(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if id !== 'new'}
|
|
||||||
<nav class="nav-side">
|
|
||||||
{#if team.id !== '0'}
|
|
||||||
<button
|
|
||||||
id="delete"
|
|
||||||
on:click={deleteTeam}
|
|
||||||
type="submit"
|
|
||||||
disabled={!$appSession.isAdmin}
|
|
||||||
class:hover:text-red-500={$appSession.isAdmin}
|
|
||||||
class="icons bg-transparent text-sm"><DeleteIcon /></button
|
|
||||||
>
|
|
||||||
<Tooltip triggeredBy="#delete">Delete</Tooltip>
|
|
||||||
{/if}
|
|
||||||
</nav>
|
|
||||||
{/if}
|
|
||||||
<slot />
|
|
108
apps/ui/src/routes/iam/teams.svelte
Normal file
108
apps/ui/src/routes/iam/teams.svelte
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<script context="module" lang="ts">
|
||||||
|
import type { Load } from '@sveltejs/kit';
|
||||||
|
export const load: Load = async () => {
|
||||||
|
try {
|
||||||
|
const response = await get(`/iam/teams`);
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
...response
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
error: new Error(error)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export let allTeams: any;
|
||||||
|
export let ownTeams: any;
|
||||||
|
import { get, post } from '$lib/api';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
import { appSession } from '$lib/store';
|
||||||
|
import { errorNotification } from '$lib/common';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
async function switchTeam(selectedTeamId: any) {
|
||||||
|
try {
|
||||||
|
const payload = await get(`/user?teamId=${selectedTeamId}`);
|
||||||
|
if (payload.token) {
|
||||||
|
Cookies.set('token', payload.token, {
|
||||||
|
path: '/'
|
||||||
|
});
|
||||||
|
$appSession.teamId = payload.teamId;
|
||||||
|
$appSession.userId = payload.userId;
|
||||||
|
$appSession.permission = payload.permission;
|
||||||
|
$appSession.isAdmin = payload.isAdmin;
|
||||||
|
return window.location.reload();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return errorNotification(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function newTeam() {
|
||||||
|
const { id } = await post('/iam/new', {});
|
||||||
|
return await goto(`/iam/teams/${id}`, { replaceState: true });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="mx-auto w-full">
|
||||||
|
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 items-center pb-3">
|
||||||
|
<div class="title font-bold">Teams</div>
|
||||||
|
<button on:click={newTeam} class="btn btn-sm btn-primary"> Add New Team </button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-col gap-4 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-2 px-6">
|
||||||
|
{#each ownTeams as team}
|
||||||
|
<a href="/iam/teams/{team.id}" class="p-2 no-underline">
|
||||||
|
<div
|
||||||
|
class="flex flex-col w-full rounded p-5 bg-coolgray-200 hover:bg-coolgray-300 indicator duration-150 h-36"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="truncate text-center text-xl font-bold">
|
||||||
|
{team.name}
|
||||||
|
{#if $appSession.teamId === team.id}
|
||||||
|
<button class="badge bg-applications text-white font-bold rounded">Active Team</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-center text-xs">
|
||||||
|
{team.permissions?.length} member(s)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center pt-3">
|
||||||
|
{#if $appSession.teamId !== team.id}
|
||||||
|
<button
|
||||||
|
on:click|preventDefault={() => switchTeam(team.id)}
|
||||||
|
class="btn btn-sm btn-primary">Switch to this team</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="divider w-32 mx-auto" />
|
||||||
|
<div class="grid grid-col gap-4 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-3 px-6">
|
||||||
|
{#if $appSession.teamId === '0' && allTeams.length > 0}
|
||||||
|
{#each allTeams as team}
|
||||||
|
<a href="/iam/teams/{team.id}" class="p-2 no-underline">
|
||||||
|
<div
|
||||||
|
class="flex flex-col w-full rounded p-5 bg-coolgray-200 hover:bg-coolgray-300 indicator duration-150 relative"
|
||||||
|
>
|
||||||
|
<div class="truncate text-center text-xl font-bold">
|
||||||
|
{team.name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-1 text-center text-xs">{team.permissions?.length} member(s)</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
31
apps/ui/src/routes/iam/teams/[id]/__layout.svelte
Normal file
31
apps/ui/src/routes/iam/teams/[id]/__layout.svelte
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script context="module" lang="ts">
|
||||||
|
import { del, get } from '$lib/api';
|
||||||
|
import type { Load } from '@sveltejs/kit';
|
||||||
|
export const load: Load = async ({ params, url }) => {
|
||||||
|
try {
|
||||||
|
const response = await get(`/iam/team/${params.id}`);
|
||||||
|
if (!response.permissions || Object.entries(response.permissions).length === 0) {
|
||||||
|
return {
|
||||||
|
status: 302,
|
||||||
|
redirect: '/iam/teams'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
...response
|
||||||
|
},
|
||||||
|
stuff: {
|
||||||
|
...response
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handlerNotFoundLoad(error, url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { handlerNotFoundLoad } from '$lib/common';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
@@ -8,16 +8,23 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
export let currentTeam: string;
|
||||||
|
export let teams: any[];
|
||||||
export let permissions: any;
|
export let permissions: any;
|
||||||
export let team: any;
|
export let team: any;
|
||||||
export let invitations: any[];
|
export let invitations: any[];
|
||||||
|
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
|
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
|
||||||
import { post } from '$lib/api';
|
import { del, get, post } from '$lib/api';
|
||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import { errorNotification } from '$lib/common';
|
import { errorNotification } from '$lib/common';
|
||||||
import { appSession } from '$lib/store';
|
import { addToast, appSession } from '$lib/store';
|
||||||
|
import Explainer from '$lib/components/Explainer.svelte';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
|
|
||||||
let invitation: any = {
|
let invitation: any = {
|
||||||
teamName: team.name,
|
teamName: team.name,
|
||||||
email: null,
|
email: null,
|
||||||
@@ -54,7 +61,7 @@
|
|||||||
}
|
}
|
||||||
async function removeFromTeam(uid: string) {
|
async function removeFromTeam(uid: string) {
|
||||||
try {
|
try {
|
||||||
await post(`/iam/team/${id}/user/remove`, { teamId: team.id, uid });
|
await post(`/iam/team/${id}/user/remove`, { uid });
|
||||||
return window.location.reload();
|
return window.location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
@@ -75,41 +82,124 @@
|
|||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
try {
|
try {
|
||||||
await post(`/iam/team/${id}`, { ...team });
|
await post(`/iam/team/${id}`, { ...team });
|
||||||
return window.location.reload();
|
return addToast({
|
||||||
|
message: 'Settings updated.',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function deleteTeam() {
|
||||||
|
const sure = confirm('Are you sure you want to delete this team?');
|
||||||
|
if (sure) {
|
||||||
|
try {
|
||||||
|
const switchTeam = teams.find((team: any) => team.id !== id);
|
||||||
|
if (!switchTeam) {
|
||||||
|
return addToast({
|
||||||
|
message: 'You cannot delete your last team.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await del(`/iam/team/${id}`, { id });
|
||||||
|
if (currentTeam === id) {
|
||||||
|
const payload = await get(`/user?teamId=${switchTeam.id}`);
|
||||||
|
if (payload.token) {
|
||||||
|
Cookies.set('token', payload.token, {
|
||||||
|
path: '/'
|
||||||
|
});
|
||||||
|
$appSession.teamId = payload.teamId;
|
||||||
|
$appSession.userId = payload.userId;
|
||||||
|
$appSession.permission = payload.permission;
|
||||||
|
$appSession.isAdmin = payload.isAdmin;
|
||||||
|
return window.location.assign('/iam');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await goto('/iam/teams', { replaceState: true });
|
||||||
|
} catch (error) {
|
||||||
|
return errorNotification(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function leaveTeam(uid: string) {
|
||||||
|
const sure = confirm('Are you sure you want to leave this team?');
|
||||||
|
if (sure) {
|
||||||
|
try {
|
||||||
|
const switchTeam = teams.find((team: any) => team.id !== id);
|
||||||
|
const foundAdmin = team.permissions.filter(
|
||||||
|
(permission: any) => permission.userId !== uid && permission.permission === 'admin'
|
||||||
|
);
|
||||||
|
if (!switchTeam) {
|
||||||
|
return addToast({
|
||||||
|
message: 'You cannot leave your last team.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!foundAdmin.length) {
|
||||||
|
return addToast({
|
||||||
|
message: 'You cannot leave this team without an admin.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await post(`/iam/team/${id}/user/remove`, { uid });
|
||||||
|
if (currentTeam === id) {
|
||||||
|
const payload = await get(`/user?teamId=${switchTeam.id}`);
|
||||||
|
if (payload.token) {
|
||||||
|
Cookies.set('token', payload.token, {
|
||||||
|
path: '/'
|
||||||
|
});
|
||||||
|
$appSession.teamId = payload.teamId;
|
||||||
|
$appSession.userId = payload.userId;
|
||||||
|
$appSession.permission = payload.permission;
|
||||||
|
$appSession.isAdmin = payload.isAdmin;
|
||||||
|
return window.location.assign('/iam');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await goto('/iam/teams', { replaceState: true });
|
||||||
|
} catch (error) {
|
||||||
|
return errorNotification(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex space-x-1 p-6 px-6 text-2xl font-bold">
|
<div class="w-full">
|
||||||
<div class="tracking-tight">{$t('index.team')}</div>
|
<div class="mx-auto w-full">
|
||||||
<span class="arrow-right-applications px-1 text-fuchsia-500">></span>
|
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 items-center pb-3">
|
||||||
<span class="pr-2">{team.name}</span>
|
<div class="title font-bold">{team.name}</div>
|
||||||
|
|
||||||
|
<button class="btn btn-sm bg-primary" on:click={handleSubmit}>{$t('forms.save')}</button>
|
||||||
|
<button
|
||||||
|
id="delete"
|
||||||
|
on:click={deleteTeam}
|
||||||
|
type="submit"
|
||||||
|
disabled={!$appSession.isAdmin}
|
||||||
|
class="btn btn-sm bg-error">Remove Team</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-auto max-w-6xl px-6">
|
|
||||||
<form on:submit|preventDefault={handleSubmit} class=" py-4">
|
<div class="mx-auto">
|
||||||
<div class="flex space-x-1 pb-5">
|
<div class="flex space-x-1 pb-5">
|
||||||
<div class="title font-bold">{$t('index.settings')}</div>
|
<div class="title font-bold">{$t('index.settings')}</div>
|
||||||
<button class="btn btn-sm bg-iam" type="submit">{$t('forms.save')}</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-flow-row gap-2 px-10">
|
<div class="grid grid-flow-row gap-2 px-4">
|
||||||
<div class="mt-2 grid grid-cols-2">
|
<div class="mt-2 grid grid-cols-2">
|
||||||
<div class="flex-col">
|
<div class="flex-col">
|
||||||
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
|
<label for="name">{$t('forms.name')}</label>
|
||||||
{#if team.id === '0'}
|
{#if team.id === '0'}
|
||||||
<SimpleExplainer customClass="w-full" text={$t('team.root_team_explainer')} />
|
<Explainer explanation={$t('team.root_team_explainer')} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<input id="name" name="name" placeholder="name" bind:value={team.name} />
|
<input id="name" name="name" placeholder="name" bind:value={team.name} class="input w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 py-5 pt-10 font-bold">
|
<div class="flex space-x-1 py-5 pt-10 font-bold">
|
||||||
<div class="title">{$t('team.members')}</div>
|
<div class="title">{$t('team.members')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-4 sm:px-6">
|
<div class="px-4">
|
||||||
<table class="w-full border-separate text-left">
|
<table class="w-full border-separate text-left">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="h-8 border-b border-coolgray-400">
|
<tr class="h-8 border-b border-coolgray-400">
|
||||||
@@ -122,25 +212,33 @@
|
|||||||
<tr class="text-xs">
|
<tr class="text-xs">
|
||||||
<td class="py-4"
|
<td class="py-4"
|
||||||
>{permission.user.email}
|
>{permission.user.email}
|
||||||
<span class="font-bold"
|
{#if permission.user.id === $appSession.userId}
|
||||||
>{permission.user.id === $appSession.userId ? $t('team.you') : ''}</span
|
<span class="font-bold badge badge-primary text-xs">{$t('team.you')}</span>
|
||||||
></td
|
{/if}
|
||||||
>
|
</td>
|
||||||
<td class="py-4">{permission.permission}</td>
|
<td class="py-4">{permission.permission}</td>
|
||||||
{#if $appSession.isAdmin && permission.user.id !== $appSession.userId && permission.permission !== 'owner'}
|
{#if $appSession.isAdmin && permission.user.id !== $appSession.userId && permission.permission !== 'owner'}
|
||||||
<td class="flex flex-col items-center justify-center space-y-2 py-4 text-center">
|
<td
|
||||||
<button
|
class="flex flex-col lg:flex-row justify-center lg:space-y-0 space-y-2 space-x-0 lg:space-x-2 text-center"
|
||||||
class="btn btn-sm btn-error"
|
|
||||||
on:click={() => removeFromTeam(permission.user.id)}>{$t('forms.remove')}</button
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm"
|
class="btn btn-sm"
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
changePermission(permission.user.id, permission.id, permission.permission)}
|
changePermission(permission.user.id, permission.id, permission.permission)}
|
||||||
>{$t('team.promote_to', {
|
>{$t('team.promote_to', {
|
||||||
grade: permission.permission === 'admin' ? 'read' : 'admin'
|
grade: permission.permission === 'admin' ? 'Read' : 'Admin'
|
||||||
})}</button
|
})}</button
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-error"
|
||||||
|
on:click={() => removeFromTeam(permission.user.id)}>{$t('forms.remove')}</button
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
{:else if permission.user.id === $appSession.userId}
|
||||||
|
<td class="py-4 flex flex-row justify-center">
|
||||||
|
<button class="btn btn-sm btn-primary" on:click={() => leaveTeam(permission.user.id)}
|
||||||
|
>Leave Team</button
|
||||||
|
>
|
||||||
</td>
|
</td>
|
||||||
{:else}
|
{:else}
|
||||||
<td class="text-center py-4 flex-col space-y-2">
|
<td class="text-center py-4 flex-col space-y-2">
|
||||||
@@ -156,9 +254,7 @@
|
|||||||
<td class="py-4 font-bold text-yellow-500">{invitation.permission}</td>
|
<td class="py-4 font-bold text-yellow-500">{invitation.permission}</td>
|
||||||
{#if isAdmin(team.permissions[0].permission)}
|
{#if isAdmin(team.permissions[0].permission)}
|
||||||
<td class="flex-col space-y-2 py-4 text-center">
|
<td class="flex-col space-y-2 py-4 text-center">
|
||||||
<button
|
<button class="btn btn-sm btn-error" on:click={() => revokeInvitation(invitation.id)}
|
||||||
class="btn btn-sm btn-error"
|
|
||||||
on:click={() => revokeInvitation(invitation.id)}
|
|
||||||
>{$t('team.revoke_invitation')}</button
|
>{$t('team.revoke_invitation')}</button
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
@@ -174,18 +270,16 @@
|
|||||||
<div class="flex space-x-1">
|
<div class="flex space-x-1">
|
||||||
<div class="flex space-x-1">
|
<div class="flex space-x-1">
|
||||||
<div class="title font-bold">{$t('team.invite_new_member')}</div>
|
<div class="title font-bold">{$t('team.invite_new_member')}</div>
|
||||||
<button class="btn btn-sm bg-iam" type="submit"
|
<button class="btn btn-sm bg-primary" type="submit">{$t('team.send_invitation')}</button>
|
||||||
>{$t('team.send_invitation')}</button
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SimpleExplainer text={$t('team.invite_only_register_explainer')} />
|
<SimpleExplainer text={$t('team.invite_only_register_explainer')} />
|
||||||
<div class="flex-col space-y-2 px-4 pt-5 sm:px-6">
|
<div class="flex-col pt-5">
|
||||||
<div class="flex space-x-0">
|
<div class="flex space-x-0">
|
||||||
<input
|
<input
|
||||||
bind:value={invitation.email}
|
bind:value={invitation.email}
|
||||||
placeholder={$t('forms.email')}
|
placeholder={$t('forms.email')}
|
||||||
class="mr-2 w-full"
|
class="input mr-2 w-full"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
@@ -373,6 +373,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</nav>
|
</nav>
|
||||||
<div class="container lg:mx-auto lg:p-0 px-8 pt-5">
|
<div class="container lg:mx-auto lg:p-0 px-8 pt-5">
|
||||||
|
{#if applications.length !== 0 || destinations.length !== 0 || databases.length !== 0 || services.length !== 0 || gitSources.length !== 0 || destinations.length !== 0}
|
||||||
<div class="space-x-2 lg:flex lg:justify-center text-center mb-4 ">
|
<div class="space-x-2 lg:flex lg:justify-center text-center mb-4 ">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-ghost"
|
class="btn btn-sm btn-ghost"
|
||||||
@@ -495,7 +496,6 @@
|
|||||||
</svg>Destinations</button
|
</svg>Destinations</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{#if applications.length !== 0 || destinations.length !== 0 || databases.length !== 0 || services.length !== 0 || gitSources.length !== 0 || destinations.length !== 0}
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<div class="input-group flex w-full">
|
<div class="input-group flex w-full">
|
||||||
<div
|
<div
|
||||||
|
@@ -4,21 +4,72 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded-box p-2 space-y-2">
|
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded-box p-2 space-y-2">
|
||||||
|
{#if $appSession.teamId === '0'}
|
||||||
<li class="menu-title">
|
<li class="menu-title">
|
||||||
<span>General</span>
|
<span>General</span>
|
||||||
</li>
|
</li>
|
||||||
{#if $appSession.teamId === '0'}
|
|
||||||
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/settings/coolify`}>
|
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/settings/coolify`}>
|
||||||
<a href={`/settings/coolify`} class="no-underline w-full">Coolify Settings</a>
|
<a href={`/settings/coolify`} class="no-underline w-full"
|
||||||
|
><svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path
|
||||||
|
d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"
|
||||||
|
/>
|
||||||
|
</svg>Coolify Settings</a
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
<li class="menu-title">
|
<li class="menu-title">
|
||||||
<span>Keys & Certificates</span>
|
<span>Keys & Certificates</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/settings/ssh`}>
|
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/settings/ssh`}>
|
||||||
<a href={`/settings/ssh`} class="no-underline w-full">SSH Keys</a>
|
<a href={`/settings/ssh`} class="no-underline w-full"
|
||||||
|
><svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<circle cx="8" cy="15" r="4" />
|
||||||
|
<line x1="10.85" y1="12.15" x2="19" y2="4" />
|
||||||
|
<line x1="18" y1="5" x2="20" y2="7" />
|
||||||
|
<line x1="15" y1="8" x2="17" y2="10" />
|
||||||
|
</svg>SSH Keys</a
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/settings/certificates`}>
|
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/settings/certificates`}>
|
||||||
<a href={`/settings/certificates`} class="no-underline w-full">SSL Certificates</a>
|
<a href={`/settings/certificates`} class="no-underline w-full">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path
|
||||||
|
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="11" r="1" />
|
||||||
|
<line x1="12" y1="12" x2="12" y2="14.5" />
|
||||||
|
</svg>SSL Certificates</a
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@@ -58,19 +58,16 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
{#if sshKeys.length === 0}
|
|
||||||
<div class="text-sm">No SSH keys found</div>
|
|
||||||
<label for="my-modal" class="btn btn-primary mt-6" on:click={() => (isModalActive = true)}
|
|
||||||
>Add SSH Key</label
|
|
||||||
>
|
|
||||||
{:else}
|
|
||||||
<div class="mx-auto w-full">
|
|
||||||
<div class="flex border-b border-coolgray-500 mb-6">
|
<div class="flex border-b border-coolgray-500 mb-6">
|
||||||
<div class="title font-bold pb-3 pr-4">SSH Keys</div>
|
<div class="title font-bold pb-3 pr-4">SSH Keys</div>
|
||||||
<label for="my-modal" class="btn btn-sm btn-primary" on:click={() => (isModalActive = true)}
|
<label for="my-modal" class="btn btn-sm btn-primary" on:click={() => (isModalActive = true)}
|
||||||
>Add SSH Key</label
|
>Add SSH Key</label
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
{#if sshKeys.length === 0}
|
||||||
|
<div class="text-sm">No SSH keys found</div>
|
||||||
|
{:else}
|
||||||
|
<div class="mx-auto w-full">
|
||||||
<table class="table w-full">
|
<table class="table w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "coolify",
|
"name": "coolify",
|
||||||
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
||||||
"version": "3.10.12",
|
"version": "3.10.13",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": "github:coollabsio/coolify",
|
"repository": "github:coollabsio/coolify",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
545
pnpm-lock.yaml
generated
545
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user