mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
171
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c136136f1 | ||
|
|
bcc9378812 | ||
|
|
cc8b9c3b86 | ||
|
|
39ac840387 | ||
|
|
195c2729b5 | ||
|
|
0e2892802f | ||
|
|
ae4ac87ebf | ||
|
|
1a210c6de9 | ||
|
|
45c82f9ef5 | ||
|
|
70a551f3e8 | ||
|
|
39ae2b8a8d | ||
|
|
4038d43220 | ||
|
|
bf70b8f0b5 | ||
|
|
1739324ee8 | ||
|
|
d6c9a7d203 | ||
|
|
d653ecd3ea | ||
|
|
4428f46f9d | ||
|
|
a4be0561eb | ||
|
|
3f76f7b604 | ||
|
|
8d448718d0 | ||
|
|
6a975629a5 | ||
|
|
f04e95f7f6 | ||
|
|
d1bd2a465c | ||
|
|
7781caac53 | ||
|
|
04c2bc8922 | ||
|
|
75dff0cc2c | ||
|
|
c8a24b53ce | ||
|
|
8cf164d229 | ||
|
|
bb62b25a55 | ||
|
|
7a9784244b | ||
|
|
bdd191eb93 | ||
|
|
25a966558a | ||
|
|
8070386172 | ||
|
|
4fecbb12f2 | ||
|
|
d47c725c44 | ||
|
|
3dca8f8f92 | ||
|
|
df989440ac | ||
|
|
ea5e89a299 | ||
|
|
84b0b4b337 | ||
|
|
2f83eafa97 | ||
|
|
9d1af49b35 | ||
|
|
b04b5ea2ee | ||
|
|
36754973c6 | ||
|
|
8c670d57c7 | ||
|
|
ebb6ba59f3 | ||
|
|
ca3535bc1c | ||
|
|
740a58679e | ||
|
|
80e468b26d | ||
|
|
0a84fd2f63 | ||
|
|
13ef2a0e6e | ||
|
|
b1b2e33839 | ||
|
|
41a3b29053 | ||
|
|
d0dafdadc7 | ||
|
|
b9d9f42ec7 | ||
|
|
0dfbe39ccd | ||
|
|
87fe60f82a | ||
|
|
7c9b38b1eb | ||
|
|
866bc76475 | ||
|
|
fd41f7ea50 | ||
|
|
bd157e7d49 | ||
|
|
fd079f4298 | ||
|
|
5d18fdf217 | ||
|
|
1b3f972bc8 | ||
|
|
68cd89fbe1 | ||
|
|
fc42afe89e | ||
|
|
a6e528d12b | ||
|
|
d309faf819 | ||
|
|
eda3c574c3 | ||
|
|
0d3135af94 | ||
|
|
870410efbf | ||
|
|
0e4b5585e3 | ||
|
|
36ed2e102c | ||
|
|
57122cf149 | ||
|
|
b0abc71ee6 | ||
|
|
50f7fb17e8 | ||
|
|
ec07424ddf | ||
|
|
972e8b1a83 | ||
|
|
1bcf7e353e | ||
|
|
9d0a0f8529 | ||
|
|
37ed654dc9 | ||
|
|
06000c4e2a | ||
|
|
a10ef50bcf | ||
|
|
69f500237a | ||
|
|
9873cd747d | ||
|
|
0a1f509e3f | ||
|
|
3b49fc5a86 | ||
|
|
ecc3747a30 | ||
|
|
6d6b2c0980 | ||
|
|
4f58488c66 | ||
|
|
a140492688 | ||
|
|
d5aa90a7d9 | ||
|
|
7674ff4625 | ||
|
|
8f413d0e1d | ||
|
|
57e78e0929 | ||
|
|
4dbf844272 | ||
|
|
86d32f3d6a | ||
|
|
0e780113de | ||
|
|
8d64376a56 | ||
|
|
e044833d20 | ||
|
|
aed8ecb53d | ||
|
|
e498aaf920 | ||
|
|
7b097c95c8 | ||
|
|
1fc04d3b0b | ||
|
|
afd0894c24 | ||
|
|
8c1025ece9 | ||
|
|
808e60557b | ||
|
|
f07ff1a28c | ||
|
|
352abb8a95 | ||
|
|
aef1c7b8fb | ||
|
|
fc63923d85 | ||
|
|
b0e23f336d | ||
|
|
1f3e36ce56 | ||
|
|
73179e4d0b | ||
|
|
18282ede01 | ||
|
|
a395f15ba5 | ||
|
|
5ac7d86b6d | ||
|
|
eaf1391601 | ||
|
|
177455db43 | ||
|
|
0fb060e2da | ||
|
|
4b5367a456 | ||
|
|
211d69c846 | ||
|
|
4e5913b0d9 | ||
|
|
32cf64eebe | ||
|
|
3d1b0aa659 | ||
|
|
49924d9dd9 | ||
|
|
4ea5babba6 | ||
|
|
19776344a3 | ||
|
|
c1b4c04a9a | ||
|
|
9cdcfb4d3f | ||
|
|
358b583634 | ||
|
|
17b9d1da02 | ||
|
|
642e6c7fde | ||
|
|
082539df12 | ||
|
|
5c1b247ee5 | ||
|
|
a50c668db7 | ||
|
|
b79d039a03 | ||
|
|
9183d6dfcb | ||
|
|
eade74be6e | ||
|
|
18e7a96754 | ||
|
|
647905da09 | ||
|
|
51317b0d08 | ||
|
|
34cce3b07d | ||
|
|
9dcde769c7 | ||
|
|
acc60f7d69 | ||
|
|
b34267ae19 | ||
|
|
fa5307c875 | ||
|
|
2d223b72c5 | ||
|
|
ed3ae9445a | ||
|
|
ca4b0fed9e | ||
|
|
40da44a03d | ||
|
|
8b97a90fb9 | ||
|
|
6e8a8ca1d7 | ||
|
|
49a6d6777a | ||
|
|
d825d5c7aa | ||
|
|
7c1707530d | ||
|
|
4c4f2a4783 | ||
|
|
f2af172f2f | ||
|
|
8e872a1fbc | ||
|
|
34ff1cb73e | ||
|
|
ed68d6c72b | ||
|
|
af79757ed8 | ||
|
|
1b7ab65aab | ||
|
|
cd4364e664 | ||
|
|
001d1b0042 | ||
|
|
a02f784c99 | ||
|
|
c549f09188 | ||
|
|
1b977fa74e | ||
|
|
89bf08ae4b | ||
|
|
708b3f3ef5 | ||
|
|
6098c2fe42 | ||
|
|
35654feedd |
@@ -81,3 +81,6 @@ TRUSTED_DOMAINS="http://localhost:8887, http://localhost:3055, http://localhost:
|
|||||||
|
|
||||||
# Default to false
|
# Default to false
|
||||||
#TUSD_BEHIND_PROXY=true
|
#TUSD_BEHIND_PROXY=true
|
||||||
|
|
||||||
|
#
|
||||||
|
#SKIP_ONBOARDING=false
|
||||||
|
|||||||
+1
-5
@@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": ["next/core-web-vitals", "plugin:tailwindcss/recommended"],
|
||||||
"next/core-web-vitals",
|
|
||||||
"plugin:tailwindcss/recommended"
|
|
||||||
],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"react/no-unescaped-entities": "off",
|
"react/no-unescaped-entities": "off",
|
||||||
"react-hooks/rules-of-hooks": "off"
|
"react-hooks/rules-of-hooks": "off"
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,3 +60,4 @@ certificates
|
|||||||
.codex
|
.codex
|
||||||
/docs
|
/docs
|
||||||
.worktrees
|
.worktrees
|
||||||
|
.superpowers
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { CardContent, CardHeader } from "@/components/ui/card";
|
import { CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { ForgotPasswordForm } from "@/features/auth/forgot-password-form";
|
import { ForgotPasswordForm } from "@/features/auth/components/forgot-password-form";
|
||||||
import { env } from "@/env.mjs";
|
import { env } from "@/env.mjs";
|
||||||
import { CardAuth } from "@/features/layout/card-auth";
|
import { CardAuth } from "@/features/layout/components/card-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default async function RoutePage(props: { searchParams: Promise<{ callbackUrl: string | undefined }> }) {
|
export default async function RoutePage(props: { searchParams: Promise<{ callbackUrl: string | undefined }> }) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {CardContent, CardHeader} from "@/components/ui/card";
|
import {CardContent, CardHeader} from "@/components/ui/card";
|
||||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||||
import {GuardForm} from "@/features/auth/guard-form";
|
import {GuardForm} from "@/features/auth/components/guard-form";
|
||||||
import {cookies} from "next/headers";
|
import {cookies} from "next/headers";
|
||||||
import {redirect} from "next/navigation";
|
import {redirect} from "next/navigation";
|
||||||
import {CardAuth} from "@/features/layout/card-auth";
|
import {CardAuth} from "@/features/layout/components/card-auth";
|
||||||
|
|
||||||
export default async function GuardPage() {
|
export default async function GuardPage() {
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { currentUser } from "@/lib/auth/current-user";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
import { AuthLogoSection } from "@/features/auth/auth-logo-section";
|
import { isOnboardingDone } from "@/db/services/setting";
|
||||||
|
import { AuthLogoSection } from "@/features/auth/components/auth-logo-section";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
import { Heart } from "lucide-react";
|
import { Heart } from "lucide-react";
|
||||||
|
|
||||||
export default async function Layout({
|
export default async function Layout({
|
||||||
@@ -9,6 +11,10 @@ export default async function Layout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
if (env.SKIP_ONBOARDING !== "true" && !(await isOnboardingDone())) {
|
||||||
|
redirect("/welcome");
|
||||||
|
}
|
||||||
|
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
|
|
||||||
if (user && !user.banned && user.role !== "pending") {
|
if (user && !user.banned && user.role !== "pending") {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { LoginForm } from "@/features/auth/login-form";
|
import { LoginForm } from "@/features/auth/components/login-form";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { SUPPORTED_PROVIDERS } from "@/lib/auth/config";
|
import { SUPPORTED_PROVIDERS } from "@/lib/auth/config";
|
||||||
import { SocialAuthButtons } from "@/features/auth/social-buttons";
|
import { SocialAuthButtons } from "@/features/auth/components/social-buttons";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { CardContent, CardHeader } from "@/components/ui/card";
|
import { CardContent, CardHeader } from "@/components/ui/card";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { CardAuth } from "@/features/layout/card-auth";
|
import { CardAuth } from "@/features/layout/components/card-auth";
|
||||||
import { env } from "@/env.mjs";
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PageParams } from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import { RegisterForm } from "@/features/auth/register-form";
|
import { RegisterForm } from "@/features/auth/components/register-form";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { env } from "@/env.mjs";
|
import { env } from "@/env.mjs";
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { CardContent, CardHeader } from "@/components/ui/card";
|
import { CardContent, CardHeader } from "@/components/ui/card";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { ResetPasswordForm } from "@/features/auth/reset-password-form";
|
import { ResetPasswordForm } from "@/features/auth/components/reset-password-form";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { auth } from "@/lib/auth/auth";
|
import { auth } from "@/lib/auth/auth";
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
|
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
|
||||||
import { CardAuth } from "@/features/layout/card-auth";
|
import { CardAuth } from "@/features/layout/components/card-auth";
|
||||||
import { env } from "@/env.mjs";
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) {
|
export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {eq} from "drizzle-orm";
|
|||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page} from "@/features/layout/page";
|
import {Page} from "@/features/layout/components/page";
|
||||||
import {OrganizationManagement} from "@/features/organizations/admin-organization-management";
|
import {OrganizationManagement} from "@/features/organizations/components/admin-organization-management";
|
||||||
import {buildOrganizationWithMembers} from "@/utils/common";
|
import {buildOrganizationWithMembers} from "@/utils/common";
|
||||||
import {isUUID} from "@/utils/text";
|
import {isUUID} from "@/utils/text";
|
||||||
import {user} from "@/db/schema/02_user";
|
import {user} from "@/db/schema/02_user";
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {
|
import {
|
||||||
AdminOrganizationAddModal
|
AdminOrganizationAddModal
|
||||||
} from "@/features/organizations/admin-organization-add-modal";
|
} from "@/features/organizations/components/admin-organization-add-modal";
|
||||||
import {AdminOrganizationList} from "@/features/organizations/admin-organization-list";
|
import {AdminOrganizationList} from "@/features/organizations/components/admin-organization-list";
|
||||||
import {isNull} from "drizzle-orm";
|
import {isNull} from "drizzle-orm";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {SettingsTabs} from "@/features/settings/settings-tabs";
|
import {SettingsTabs} from "@/features/settings/components/settings-tabs";
|
||||||
import {desc, isNull} from "drizzle-orm";
|
import {desc, isNull} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {desc, isNull} from "drizzle-orm";
|
import {desc, isNull} from "drizzle-orm";
|
||||||
import {AdminUserList} from "@/features/users/admin-user-list";
|
import {AdminUserList} from "@/features/users/components/admin-user-list";
|
||||||
import {AdminUserAddModal} from "@/features/users/admin-user-add-modal";
|
import {AdminUserAddModal} from "@/features/users/components/admin-user-add-modal";
|
||||||
import {SUPPORTED_PROVIDERS} from "@/lib/auth/config";
|
import {SUPPORTED_PROVIDERS} from "@/lib/auth/config";
|
||||||
|
import {getSettings} from "@/db/services/setting";
|
||||||
|
import {resolveAvatarUrl} from "@/utils/resolve-avatar-url";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
const users = await db.query.user.findMany({
|
const [settings, users] = await Promise.all([
|
||||||
|
getSettings(),
|
||||||
|
db.query.user.findMany({
|
||||||
where: (fields) => isNull(fields.deletedAt),
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
with: {
|
with: { accounts: true },
|
||||||
accounts: true
|
|
||||||
},
|
|
||||||
orderBy: (fields) => desc(fields.createdAt),
|
orderBy: (fields) => desc(fields.createdAt),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const avatarUrls = Object.fromEntries(
|
||||||
|
users.map((u) => [u.id, resolveAvatarUrl(u, settings)])
|
||||||
|
);
|
||||||
|
|
||||||
});
|
|
||||||
const organizations = await db.query.organization.findMany({
|
const organizations = await db.query.organization.findMany({
|
||||||
with: {
|
with: {
|
||||||
members: true,
|
members: true,
|
||||||
@@ -38,7 +45,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="flex flex-col gap-5">
|
<PageContent className="flex flex-col gap-5">
|
||||||
<AdminUserList users={users} isPasswordAuthEnabled={isPasswordAuthEnabled}/>
|
<AdminUserList users={users} isPasswordAuthEnabled={isPasswordAuthEnabled} avatarUrls={avatarUrls}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import {
|
|||||||
PageContent,
|
PageContent,
|
||||||
PageDescription,
|
PageDescription,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
} from "@/features/layout/page";
|
} from "@/features/layout/components/page";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {eq, isNull} from "drizzle-orm";
|
import {eq, isNull} from "drizzle-orm";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { ButtonDeleteAgent } from "@/features/agents/agent-delete-button";
|
import { ButtonDeleteAgent } from "@/features/agents/components/agent-delete-button";
|
||||||
import { capitalizeFirstLetter } from "@/utils/text";
|
import { capitalizeFirstLetter } from "@/utils/text";
|
||||||
import { generateEdgeKey } from "@/utils/edge_key";
|
import { generateEdgeKey } from "@/utils/edge_key";
|
||||||
import { getServerUrl } from "@/utils/get-server-url";
|
import { getServerUrl } from "@/utils/get-server-url";
|
||||||
import { AgentContentPage } from "@/features/agents/agent-content";
|
import { AgentContentPage } from "@/features/agents/components/agent-content";
|
||||||
import { AgentDialog } from "@/features/agents/agent-dialog";
|
import { AgentDialog } from "@/features/agents/components/agent-dialog";
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(
|
export default async function RoutePage(
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {AgentCard} from "@/features/agents/agent-card";
|
import {AgentCard} from "@/features/agents/components/agent-card";
|
||||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {and, desc, eq, isNull, not} from "drizzle-orm";
|
import {and, desc, eq, isNull, not} from "drizzle-orm";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
import {AgentDialog} from "@/features/agents/agent-dialog";
|
import {AgentDialog} from "@/features/agents/components/agent-dialog";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Agents",
|
title: "Agents",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {notificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
import {notificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
import {desc, isNull} from "drizzle-orm";
|
import {desc, isNull} from "drizzle-orm";
|
||||||
import {ChannelsSection} from "@/features/channel/channels-section";
|
import {ChannelsSection} from "@/features/channel/components/channels-section";
|
||||||
import {ChannelAddEditModal} from "@/features/channel/channel-add-edit-modal";
|
import {ChannelAddEditModal} from "@/features/channel/components/channel-add-edit-modal";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
import {NotificationLogsList} from "@/features/notifications/notification-logs-list";
|
import {NotificationLogsList} from "@/features/notifications/components/notification-logs-list";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {getNotificationHistory} from "@/db/services/notification-log";
|
import {getNotificationHistory} from "@/db/services/notification-log";
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
import {ChannelsSection} from "@/features/channel/channels-section";
|
import {ChannelsSection} from "@/features/channel/components/channels-section";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {desc, eq, isNull} from "drizzle-orm";
|
import {desc, eq, isNull} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
import {ChannelAddEditModal} from "@/features/channel/channel-add-edit-modal";
|
import {ChannelAddEditModal} from "@/features/channel/components/channel-add-edit-modal";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Storage Channels",
|
title: "Storage Channels",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getOrganization} from "@/lib/auth/auth";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {MigrationTool} from "@/features/migration/migration-tool";
|
import {MigrationTool} from "@/features/migration/components/migration-tool";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Projects",
|
title: "Projects",
|
||||||
|
|||||||
+56
-41
@@ -1,24 +1,26 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {notFound, redirect} from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import {Page} from "@/features/layout/page";
|
import { Page } from "@/features/layout/components/page";
|
||||||
import {db} from "@/db";
|
import { db } from "@/db";
|
||||||
import {eq, and, inArray} from "drizzle-orm";
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {getOrganizationProjectDatabases} from "@/db/services/project";
|
import { getOrganizationProjectDatabases } from "@/db/services/project";
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import { getActiveMember, getOrganization } from "@/lib/auth/auth";
|
||||||
import {BackupModalProvider} from "@/features/database/backup-modal-context";
|
import { BackupModalProvider } from "@/features/database/components/backup-modal-context";
|
||||||
import {DatabaseContent} from "@/features/database/database-content";
|
import { DatabaseContent } from "@/features/database/components/database-content";
|
||||||
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
|
import { getHealthLast12hLogs } from "@/db/services/healthcheck";
|
||||||
import {LogsModalProvider} from "@/features/logs/logs-modal-context";
|
import { LogsModalProvider } from "@/features/logs/components/logs-modal-context";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
export default async function RoutePage(
|
||||||
|
props: PageParams<{
|
||||||
projectId: string;
|
projectId: string;
|
||||||
databaseId: string
|
databaseId: string;
|
||||||
}>) {
|
}>,
|
||||||
const {projectId, databaseId} = await props.params;
|
) {
|
||||||
|
const { projectId, databaseId } = await props.params;
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
const activeMember = await getActiveMember()
|
const activeMember = await getActiveMember();
|
||||||
|
|
||||||
if (!organization || !activeMember) {
|
if (!organization || !activeMember) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -26,17 +28,21 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
|
|
||||||
const databasesProject = await getOrganizationProjectDatabases({
|
const databasesProject = await getOrganizationProjectDatabases({
|
||||||
organizationSlug: organization.slug,
|
organizationSlug: organization.slug,
|
||||||
projectId: projectId
|
projectId: projectId,
|
||||||
})
|
});
|
||||||
|
|
||||||
const dbItem = await db.query.database.findFirst({
|
const dbItem = await db.query.database.findFirst({
|
||||||
where: and(inArray(drizzleDb.schemas.backup.id, databasesProject.ids ?? []), eq(drizzleDb.schemas.database.id, databaseId), eq(drizzleDb.schemas.database.projectId, projectId)),
|
where: and(
|
||||||
|
inArray(drizzleDb.schemas.backup.id, databasesProject.ids ?? []),
|
||||||
|
eq(drizzleDb.schemas.database.id, databaseId),
|
||||||
|
eq(drizzleDb.schemas.database.projectId, projectId),
|
||||||
|
),
|
||||||
with: {
|
with: {
|
||||||
project: true,
|
project: true,
|
||||||
retentionPolicy: true,
|
retentionPolicy: true,
|
||||||
alertPolicies: true,
|
alertPolicies: true,
|
||||||
storagePolicies: true,
|
storagePolicies: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!dbItem) {
|
if (!dbItem) {
|
||||||
@@ -49,52 +55,61 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
restorations: true,
|
restorations: true,
|
||||||
storages: {
|
storages: {
|
||||||
with: {
|
with: {
|
||||||
storageChannel: true
|
storageChannel: true,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
logs: true
|
|
||||||
},
|
},
|
||||||
orderBy: (b, {desc}) => [desc(b.createdAt)],
|
logs: true,
|
||||||
|
},
|
||||||
|
orderBy: (b, { desc }) => [desc(b.createdAt)],
|
||||||
});
|
});
|
||||||
|
|
||||||
const restorations = await db.query.restoration.findMany({
|
const restorations = await db.query.restoration.findMany({
|
||||||
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
|
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
|
||||||
with: {
|
with: {
|
||||||
logs: true
|
logs: true,
|
||||||
},
|
},
|
||||||
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
orderBy: (r, { desc }) => [desc(r.createdAt)],
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing");
|
//const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing");
|
||||||
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
||||||
|
|
||||||
const totalBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
const totalBackups = await db
|
||||||
|
.select({ count: drizzleDb.schemas.backup.id })
|
||||||
.from(drizzleDb.schemas.backup)
|
.from(drizzleDb.schemas.backup)
|
||||||
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
|
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
|
||||||
.then(rows => rows.length);
|
.then((rows) => rows.length);
|
||||||
|
|
||||||
const availableBackups = backups.filter(b => !b.deletedAt).length;
|
const availableBackups = backups.filter((b) => !b.deletedAt).length;
|
||||||
|
|
||||||
const successfulBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
const successfulBackups = await db
|
||||||
|
.select({ count: drizzleDb.schemas.backup.id })
|
||||||
.from(drizzleDb.schemas.backup)
|
.from(drizzleDb.schemas.backup)
|
||||||
.where(and(
|
.where(
|
||||||
|
and(
|
||||||
eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
|
eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
|
||||||
eq(drizzleDb.schemas.backup.status, "success")
|
eq(drizzleDb.schemas.backup.status, "success"),
|
||||||
))
|
),
|
||||||
.then(rows => rows.length);
|
)
|
||||||
|
.then((rows) => rows.length);
|
||||||
|
|
||||||
|
const [settings] = await db
|
||||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
.select()
|
||||||
|
.from(drizzleDb.schemas.setting)
|
||||||
|
.where(eq(drizzleDb.schemas.setting.name, "system"))
|
||||||
|
.limit(1);
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const databaseHealthLogs = dbItem ? await getHealthLast12hLogs({id: dbItem.id}) : []
|
const databaseHealthLogs = dbItem
|
||||||
|
? await getHealthLast12hLogs({ id: dbItem.id })
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const successRate =
|
||||||
|
totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||||
|
|
||||||
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
//const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
const isMember = activeMember?.role === "member";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
|
|||||||
@@ -1,35 +1,31 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {Page, PageContent, PageTitle} from "@/features/layout/page";
|
import { Page, PageContent, PageTitle } from "@/features/layout/components/page";
|
||||||
import {
|
import { ButtonDeleteProject } from "@/features/projects/components/project-delete-button";
|
||||||
ButtonDeleteProject
|
import { CardsWithPagination } from "@/components/common/cards-with-pagination";
|
||||||
} from "@/features/projects/project-delete-button";
|
import { ProjectDatabaseCard } from "@/features/projects/components/project-database-card";
|
||||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import {ProjectDatabaseCard} from "@/features/projects/project-database-card";
|
import { db } from "@/db";
|
||||||
import {notFound, redirect} from "next/navigation";
|
import { eq } from "drizzle-orm";
|
||||||
import {db} from "@/db";
|
import { getActiveMember, getOrganization } from "@/lib/auth/auth";
|
||||||
import {eq} from "drizzle-orm";
|
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {capitalizeFirstLetter} from "@/utils/text";
|
import { capitalizeFirstLetter, isUUID } from "@/utils/text";
|
||||||
import {ProjectDialog} from "@/features/projects/project-dialog";
|
import { ProjectDialog } from "@/features/projects/components/project-dialog";
|
||||||
import {ProjectWith} from "@/db/schema/06_project";
|
import { ProjectWith } from "@/db/schema/06_project";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import { getOrganizationAvailableDatabases } from "@/db/services/database";
|
||||||
import {getOrganizationAvailableDatabases} from "@/db/services/database";
|
|
||||||
|
|
||||||
|
export default async function RoutePage(
|
||||||
|
props: PageParams<{
|
||||||
|
projectId: string;
|
||||||
|
}>,
|
||||||
|
) {
|
||||||
|
const { projectId } = await props.params;
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
if (!isUUID(projectId)) {
|
||||||
projectId: string
|
notFound();
|
||||||
}>) {
|
|
||||||
const {
|
|
||||||
projectId
|
|
||||||
} = await props.params;
|
|
||||||
|
|
||||||
if (!isUuidv4(projectId)) {
|
|
||||||
notFound()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
const activeMember = await getActiveMember()
|
const activeMember = await getActiveMember();
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -41,11 +37,12 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
if (!org) notFound();
|
if (!org) notFound();
|
||||||
|
|
||||||
const proj = await db.query.project.findFirst({
|
const proj = await db.query.project.findFirst({
|
||||||
where: (proj, {
|
where: (proj, { and, eq, not }) =>
|
||||||
and,
|
and(
|
||||||
eq,
|
eq(proj.id, projectId),
|
||||||
not
|
eq(proj.organizationId, org.id),
|
||||||
}) => and(eq(proj.id, projectId), eq(proj.organizationId, org.id), not(eq(proj.isArchived, true))),
|
not(eq(proj.isArchived, true)),
|
||||||
|
),
|
||||||
with: {
|
with: {
|
||||||
databases: true,
|
databases: true,
|
||||||
},
|
},
|
||||||
@@ -55,7 +52,10 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
redirect("/dashboard/projects");
|
redirect("/dashboard/projects");
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableDatabases = await getOrganizationAvailableDatabases(organization.id, proj.id)
|
const availableDatabases = await getOrganizationAvailableDatabases(
|
||||||
|
organization.id,
|
||||||
|
proj.id,
|
||||||
|
);
|
||||||
const isMember = activeMember?.role === "member";
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -76,7 +76,10 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
<ButtonDeleteProject
|
||||||
|
projectId={projectId}
|
||||||
|
text={"Delete Project"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -85,8 +88,10 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
<PageContent className="flex flex-col w-full h-full">
|
<PageContent className="flex flex-col w-full h-full">
|
||||||
{proj.databases.length > 0 ? (
|
{proj.databases.length > 0 ? (
|
||||||
<CardsWithPagination
|
<CardsWithPagination
|
||||||
data={[...proj.databases].sort((a, b) =>
|
data={[...proj.databases].sort(
|
||||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
(a, b) =>
|
||||||
|
new Date(b.createdAt).getTime() -
|
||||||
|
new Date(a.createdAt).getTime(),
|
||||||
)}
|
)}
|
||||||
organizationSlug={organization.slug}
|
organizationSlug={organization.slug}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -99,7 +104,9 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground py-20">
|
<div className="flex flex-col items-center justify-center h-full text-muted-foreground py-20">
|
||||||
<p className="text-lg font-medium">No databases found</p>
|
<p className="text-lg font-medium">No databases found</p>
|
||||||
<p className="text-sm mt-2">You haven’t added any databases to this project yet.</p>
|
<p className="text-sm mt-2">
|
||||||
|
You haven’t added any databases to this project yet.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||||
import {ProjectCard} from "@/features/projects/project-card";
|
import {ProjectCard} from "@/features/projects/components/project-card";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {EmptyStatePlaceholder} from "@/components/common/empty-state-placeholder";
|
import {EmptyStatePlaceholder} from "@/components/common/empty-state-placeholder";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
import {ProjectDialog} from "@/features/projects/project-dialog";
|
import {ProjectDialog} from "@/features/projects/components/project-dialog";
|
||||||
import {DatabaseWith} from "@/db/schema/07_database";
|
import {DatabaseWith} from "@/db/schema/07_database";
|
||||||
import {getOrganizationAvailableDatabases} from "@/db/services/database";
|
import {getOrganizationAvailableDatabases} from "@/db/services/database";
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import {
|
|||||||
PageContent,
|
PageContent,
|
||||||
PageDescription,
|
PageDescription,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
} from "@/features/layout/page";
|
} from "@/features/layout/components/page";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {ButtonDeleteAgent} from "@/features/agents/agent-delete-button";
|
import {ButtonDeleteAgent} from "@/features/agents/components/agent-delete-button";
|
||||||
import {capitalizeFirstLetter} from "@/utils/text";
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
import {generateEdgeKey} from "@/utils/edge_key";
|
import {generateEdgeKey} from "@/utils/edge_key";
|
||||||
import {getServerUrl} from "@/utils/get-server-url";
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
import {AgentContentPage} from "@/features/agents/agent-content";
|
import {AgentContentPage} from "@/features/agents/components/agent-content";
|
||||||
import {AgentDialog} from "@/features/agents/agent-dialog";
|
import {AgentDialog} from "@/features/agents/components/agent-dialog";
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import {
|
|||||||
PageContent,
|
PageContent,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
} from "@/features/layout/page";
|
} from "@/features/layout/components/page";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
import {OrganizationTabs} from "@/features/organizations/organization-tabs";
|
import {OrganizationTabs} from "@/features/organizations/components/organization-tabs";
|
||||||
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
||||||
import {DeleteOrganizationButton} from "@/features/organizations/organization-delete-button";
|
import {DeleteOrganizationButton} from "@/features/organizations/components/organization-delete-button";
|
||||||
import {EditOrganizationDialog} from "@/features/organizations/organization-edit-dialog";
|
import {EditOrganizationDialog} from "@/features/organizations/components/organization-edit-dialog";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {isNull} from "drizzle-orm";
|
import {isNull} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import {EvolutionLineChart} from "@/features/statistics/evolution-line-chart";
|
import {EvolutionLineChart} from "@/features/statistics/components/evolution-line-chart";
|
||||||
import {PercentageLineChart} from "@/features/statistics/percentage-line-chart";
|
import {PercentageLineChart} from "@/features/statistics/components/percentage-line-chart";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {and, asc, count, eq, inArray} from "drizzle-orm";
|
import {and, asc, count, eq, inArray} from "drizzle-orm";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import {Building2, Database, DatabaseBackup, Folder, RefreshCcw, Server, Workflow} from "lucide-react";
|
import {Building2, Database, DatabaseBackup, Folder, RefreshCcw, Server, Workflow} from "lucide-react";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
|
|||||||
@@ -2,21 +2,25 @@ import { ReactNode } from "react";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||||
import { AppSidebar } from "@/features/layout/app-sidebar";
|
import { AppSidebar } from "@/features/layout/components/app-sidebar";
|
||||||
import { Header } from "@/features/layout/header";
|
import { Header } from "@/features/layout/components/header";
|
||||||
import { currentUser } from "@/lib/auth/current-user";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
import { ThemeMetaUpdater } from "@/features/theme/theme-meta-updater";
|
import { isOnboardingDone } from "@/db/services/setting";
|
||||||
import { ModeToggle } from "@/features/theme/mode-toggle";
|
import { env } from "@/env.mjs";
|
||||||
import { UpdateNotification } from "@/features/updates/update-notification";
|
import { ModeToggle } from "@/features/theme/components/mode-toggle";
|
||||||
|
import { UpdateNotification } from "@/features/updates/components/update-notification";
|
||||||
|
|
||||||
export default async function Layout({ children }: { children: ReactNode }) {
|
export default async function Layout({ children }: { children: ReactNode }) {
|
||||||
|
if (env.SKIP_ONBOARDING !== "true" && !(await isOnboardingDone())) {
|
||||||
|
redirect("/welcome");
|
||||||
|
}
|
||||||
|
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
if (!user) redirect("/login");
|
if (!user) redirect("/login");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<div className="flex flex-col lg:flex-row w-full">
|
<div className="flex flex-col lg:flex-row w-full">
|
||||||
<ThemeMetaUpdater />
|
|
||||||
<AppSidebar updateNotification={<UpdateNotification />} />
|
<AppSidebar updateNotification={<UpdateNotification />} />
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<Header actions={<ModeToggle />} />
|
<Header actions={<ModeToggle />} />
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getCurrentOrganizationSlug } from "@/features/organizations/organization-cookie";
|
import { getCurrentOrganizationSlug } from "@/features/organizations/utils/organization-cookie";
|
||||||
import { currentUser } from "@/lib/auth/current-user";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
|
import { isOnboardingDone } from "@/db/services/setting";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export default async function Index() {
|
export default async function Index() {
|
||||||
|
if (env.SKIP_ONBOARDING !== "true" && !(await isOnboardingDone())) {
|
||||||
|
redirect("/welcome");
|
||||||
|
}
|
||||||
|
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
if (user) {
|
if (user) {
|
||||||
const currentOrganizationSlug = await getCurrentOrganizationSlug();
|
const currentOrganizationSlug = await getCurrentOrganizationSlug();
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { OnboardingProvider } from "@onboardjs/react";
|
||||||
|
import { onboardingSteps } from "@/features/onboarding/onboarding-steps";
|
||||||
|
import { OnboardingShell } from "@/features/onboarding/onboarding-shell";
|
||||||
|
import type { OnboardingFlowData } from "@/features/onboarding/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
initialStepId: string;
|
||||||
|
initialFlowData: Partial<OnboardingFlowData>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OnboardingClient = ({ initialStepId, initialFlowData }: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OnboardingProvider
|
||||||
|
steps={onboardingSteps}
|
||||||
|
initialStepId={initialStepId}
|
||||||
|
initialContext={{ flowData: initialFlowData }}
|
||||||
|
onFlowComplete={async () => {
|
||||||
|
router.push("/dashboard/home");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OnboardingShell />
|
||||||
|
</OnboardingProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { resolveOnboardingState } from "@/features/onboarding/onboarding-state";
|
||||||
|
import { OnboardingClient } from "./onboarding-client";
|
||||||
|
|
||||||
|
export default async function WelcomePage() {
|
||||||
|
const result = await resolveOnboardingState();
|
||||||
|
|
||||||
|
if ("redirect" in result) {
|
||||||
|
redirect(result.redirect);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OnboardingClient
|
||||||
|
key={result.stepId}
|
||||||
|
initialStepId={result.stepId}
|
||||||
|
initialFlowData={result.flowData}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ import {getDatabaseOrThrow, withAgentCheck} from "./helpers";
|
|||||||
import {Backup} from "@/db/schema/07_database";
|
import {Backup} from "@/db/schema/07_database";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
import {eventEmitter} from "@/lib/event";
|
import {eventEmitter} from "@/lib/event";
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
|
import {sendNotificationsBackupRestore} from "@/features/notifications/utils/notifications.helpers";
|
||||||
import {EventKind} from "@/features/notifications/notifications.types";
|
import {EventKind} from "@/features/notifications/types";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
import {JobLogEntry} from "@/features/logs/types";
|
import {JobLogEntry} from "@/features/logs/types";
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,43 @@
|
|||||||
import {NextResponse} from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import {and, eq} from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import { db } from "@/db";
|
||||||
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
import { getDatabaseOrThrow, withAgentCheck } from "../../helpers";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import { eventEmitter } from "@/lib/event";
|
||||||
import {eventEmitter} from "@/lib/event";
|
import { logger } from "@/lib/logger";
|
||||||
import {logger} from "@/lib/logger";
|
import { isUUID } from "@/utils/text";
|
||||||
|
|
||||||
const log = logger.child({module: "api/agent/backup/upload/init"});
|
const log = logger.child({ module: "api/agent/backup/upload/init" });
|
||||||
|
|
||||||
export type Body = {
|
export type Body = {
|
||||||
generatedId: string
|
generatedId: string;
|
||||||
storageChannelId: string
|
storageChannelId: string;
|
||||||
backupId: string
|
backupId: string;
|
||||||
}
|
};
|
||||||
export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
export const POST = withAgentCheck(
|
||||||
params: Promise<{ agentId: string }>,
|
async (
|
||||||
agent: any
|
request: Request,
|
||||||
}) => {
|
{
|
||||||
|
params,
|
||||||
|
agent,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ agentId: string }>;
|
||||||
|
agent: any;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const body: Body = await request.json();
|
const body: Body = await request.json();
|
||||||
|
|
||||||
log.info({data: body}, "Body for backup upload init");
|
log.info({ data: body }, "Body for backup upload init");
|
||||||
|
|
||||||
const generatedId = body.generatedId;
|
const generatedId = body.generatedId;
|
||||||
const storageChannelId = body.storageChannelId;
|
const storageChannelId = body.storageChannelId;
|
||||||
const backupId = body.backupId;
|
const backupId = body.backupId;
|
||||||
|
|
||||||
if (!generatedId || !isUuidv4(generatedId)) {
|
if (!generatedId || !isUUID(generatedId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "generatedId is not a valid UUID"},
|
{ error: "generatedId is not a valid UUID" },
|
||||||
{status: 400}
|
{ status: 400 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,8 +52,8 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
|
|
||||||
if (!backup) {
|
if (!backup) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Unable to find the corresponding backup"},
|
{ error: "Unable to find the corresponding backup" },
|
||||||
{status: 404}
|
{ status: 404 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,21 +66,21 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
eventEmitter.emit("modification", { update: true });
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
message: "Backup storage successfully created",
|
message: "Backup storage successfully created",
|
||||||
backupStorage: backupStorage
|
backupStorage: backupStorage,
|
||||||
},
|
},
|
||||||
{status: 200}
|
{ status: 200 },
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({error: error}, "Error in POST for INIT backup");
|
log.error({ error: error }, "Error in POST for INIT backup");
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Internal server error"},
|
{ error: "Internal server error" },
|
||||||
{status: 500}
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,73 +1,86 @@
|
|||||||
import {NextResponse} from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db as dbClient, db} from "@/db";
|
import { db as dbClient, db } from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
|
import { sendNotificationsBackupRestore } from "@/features/notifications/utils/notifications.helpers";
|
||||||
import {logger} from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import { withUpdatedAt } from "@/db/utils";
|
||||||
import {JobLogEntry} from "@/features/logs/types";
|
import { JobLogEntry } from "@/features/logs/types";
|
||||||
|
import { isUUID } from "@/utils/text";
|
||||||
|
|
||||||
const log = logger.child({module: "api/agent/restore"});
|
const log = logger.child({ module: "api/agent/restore" });
|
||||||
|
|
||||||
export type BodyResultRestore = {
|
export type BodyResultRestore = {
|
||||||
generatedId: string
|
generatedId: string;
|
||||||
status: string
|
status: string;
|
||||||
logs: JobLogEntry[]
|
logs: JobLogEntry[];
|
||||||
durationMs: number
|
durationMs: number;
|
||||||
}
|
};
|
||||||
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
|
type RestorationStatus = "waiting" | "ongoing" | "failed" | "success";
|
||||||
|
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: Request,
|
request: Request,
|
||||||
{params}: { params: Promise<{ agentId: string }> }
|
{ params }: { params: Promise<{ agentId: string }> },
|
||||||
) {
|
) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const agentId = (await params).agentId;
|
||||||
const agentId = (await params).agentId
|
|
||||||
const body: BodyResultRestore = await request.json();
|
const body: BodyResultRestore = await request.json();
|
||||||
|
|
||||||
|
if (!isUUID(body.generatedId)) {
|
||||||
if (!isUuidv4(body.generatedId)) {
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "generatedId is not a valid uuid"},
|
{ error: "generatedId is not a valid uuid" },
|
||||||
{status: 500}
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
const agent = await db.query.agent.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.agent.id, agentId), eq(drizzleDb.schemas.agent.isArchived, false)),
|
where: and(
|
||||||
})
|
eq(drizzleDb.schemas.agent.id, agentId),
|
||||||
|
eq(drizzleDb.schemas.agent.isArchived, false),
|
||||||
|
),
|
||||||
|
});
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
return NextResponse.json({error: "Agent not found"}, {status: 404})
|
return NextResponse.json({ error: "Agent not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const database = await db.query.database.findFirst({
|
const database = await db.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, body.generatedId),
|
where: eq(drizzleDb.schemas.database.agentDatabaseId, body.generatedId),
|
||||||
with: {
|
with: {
|
||||||
alertPolicies: true
|
alertPolicies: true,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!database) {
|
if (!database) {
|
||||||
return NextResponse.json({error: "Database associated with generatedId provided not found"}, {status: 404})
|
return NextResponse.json(
|
||||||
|
{ error: "Database associated with generatedId provided not found" },
|
||||||
|
{ status: 404 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const restoration = await db.query.restoration.findFirst({
|
const restoration = await db.query.restoration.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.restoration.status, "ongoing"), eq(drizzleDb.schemas.restoration.databaseId, database.id),)
|
where: and(
|
||||||
})
|
eq(drizzleDb.schemas.restoration.status, "ongoing"),
|
||||||
|
eq(drizzleDb.schemas.restoration.databaseId, database.id),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
if (!restoration) {
|
if (!restoration) {
|
||||||
return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404})
|
return NextResponse.json(
|
||||||
|
{ error: "Unable to fin the corresponding restoration" },
|
||||||
|
{ status: 404 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [restorationUpdated] = await db
|
const [restorationUpdated] = await db
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set(withUpdatedAt({status: body.status as RestorationStatus, durationMs: body.durationMs}))
|
.set(
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id)).returning();
|
withUpdatedAt({
|
||||||
|
status: body.status as RestorationStatus,
|
||||||
|
durationMs: body.durationMs,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
const logsToInsert = body.logs.map((entry) => ({
|
const logsToInsert = body.logs.map((entry) => ({
|
||||||
backupId: null,
|
backupId: null,
|
||||||
@@ -87,24 +100,25 @@ export async function POST(
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
if (logsToInsert.length > 0) {
|
if (logsToInsert.length > 0) {
|
||||||
await dbClient
|
await dbClient.insert(drizzleDb.schemas.jobLog).values(logsToInsert);
|
||||||
.insert(drizzleDb.schemas.jobLog)
|
|
||||||
.values(logsToInsert);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore");
|
await sendNotificationsBackupRestore(
|
||||||
|
database,
|
||||||
|
body.status == "failed" ? "error_restore" : "success_restore",
|
||||||
|
);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
status: true,
|
status: true,
|
||||||
message: "Restoration successfully updated"
|
message: "Restoration successfully updated",
|
||||||
}
|
};
|
||||||
|
|
||||||
return Response.json(response, {status: 200})
|
return Response.json(response, { status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({error: error}, "Error in POST handler")
|
log.error({ error: error }, "Error in POST handler");
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: 'Internal server error'},
|
{ error: "Internal server error" },
|
||||||
{status: 500}
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {Body} from "./route";
|
import {Body} from "./route";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
|
||||||
import {Agent} from "@/db/schema/08_agent";
|
import {Agent} from "@/db/schema/08_agent";
|
||||||
import {DatabaseWith} from "@/db/schema/07_database";
|
import {DatabaseWith} from "@/db/schema/07_database";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
@@ -8,10 +7,11 @@ import {db, db as dbClient} from "@/db";
|
|||||||
import {and, eq, inArray} from "drizzle-orm";
|
import {and, eq, inArray} from "drizzle-orm";
|
||||||
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
import type {StorageInput} from "@/features/storages/storages.types";
|
|
||||||
import {dispatchStorage} from "@/features/storages/storages.dispatch";
|
|
||||||
import {Setting} from "@/db/schema/01_setting";
|
import {Setting} from "@/db/schema/01_setting";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
import {isUUID} from "@/utils/text";
|
||||||
|
import {StorageInput} from "@/features/storages/types";
|
||||||
|
import {dispatchStorage} from "@/features/storages/utils/storages.dispatch";
|
||||||
|
|
||||||
const log = logger.child({module: "api/agent/status/helpers"});
|
const log = logger.child({module: "api/agent/status/helpers"});
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
let backupSize: number | null = null
|
let backupSize: number | null = null
|
||||||
|
|
||||||
if (!existingDatabase) {
|
if (!existingDatabase) {
|
||||||
if (!isUuidv4(db.generatedId)) {
|
if (!isUUID(db.generatedId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "generatedId is not a valid uuid"},
|
{error: "generatedId is not a valid uuid"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
|
|||||||
@@ -1,99 +1,107 @@
|
|||||||
import {NextResponse} from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import {handleDatabases} from "./helpers";
|
import { handleDatabases } from "./helpers";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import { db } from "@/db";
|
||||||
import {EDbmsSchema} from "@/db/schema/types";
|
import { EDbmsSchema } from "@/db/schema/types";
|
||||||
import {and, eq} from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import { withUpdatedAt } from "@/db/utils";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import { logger } from "@/lib/logger";
|
||||||
import {logger} from "@/lib/logger";
|
import { isUUID } from "@/utils/text";
|
||||||
|
|
||||||
|
|
||||||
const log = logger.child({module: "api/agent/status/route"});
|
|
||||||
|
|
||||||
|
const log = logger.child({ module: "api/agent/status/route" });
|
||||||
|
|
||||||
export type databaseAgent = {
|
export type databaseAgent = {
|
||||||
name: string,
|
name: string;
|
||||||
dbms: EDbmsSchema,
|
dbms: EDbmsSchema;
|
||||||
generatedId: string
|
generatedId: string;
|
||||||
pingStatus: boolean
|
pingStatus: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Body = {
|
export type Body = {
|
||||||
version: string,
|
version: string;
|
||||||
databases: databaseAgent[]
|
databases: databaseAgent[];
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: Request,
|
request: Request,
|
||||||
{params}: { params: Promise<{ agentId: string }> }
|
{ params }: { params: Promise<{ agentId: string }> },
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const agentId = (await params).agentId
|
const agentId = (await params).agentId;
|
||||||
log.debug(`Agent ID: ${agentId}`)
|
log.debug(`Agent ID: ${agentId}`);
|
||||||
const body: Body = await request.json();
|
const body: Body = await request.json();
|
||||||
const lastContact = new Date();
|
const lastContact = new Date();
|
||||||
let message: string
|
let message: string;
|
||||||
|
|
||||||
if (!isUuidv4(agentId)) {
|
if (!isUUID(agentId)) {
|
||||||
message = "agentId is not a valid uuid"
|
message = "agentId is not a valid uuid";
|
||||||
log.error({error: message}, "An error occurred")
|
log.error({ error: message }, "An error occurred");
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "agentId is not a valid uuid"},
|
{ error: "agentId is not a valid uuid" },
|
||||||
{status: 500}
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
const agent = await db.query.agent.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.agent.id, agentId), eq(drizzleDb.schemas.agent.isArchived, false)),
|
where: and(
|
||||||
})
|
eq(drizzleDb.schemas.agent.id, agentId),
|
||||||
|
eq(drizzleDb.schemas.agent.isArchived, false),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
message = "Agent not found"
|
message = "Agent not found";
|
||||||
return NextResponse.json({error: message}, {status: 404})
|
return NextResponse.json({ error: message }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
const [settings] = await db
|
||||||
|
.select()
|
||||||
|
.from(drizzleDb.schemas.setting)
|
||||||
|
.where(eq(drizzleDb.schemas.setting.name, "system"))
|
||||||
|
.limit(1);
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
return NextResponse.json({error: "An error occured"}, {status: 404})
|
return NextResponse.json({ error: "An error occured" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const databasesResponse = await handleDatabases(body, agent, lastContact, settings)
|
const databasesResponse = await handleDatabases(
|
||||||
|
body,
|
||||||
|
agent,
|
||||||
|
lastContact,
|
||||||
|
settings,
|
||||||
|
);
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(drizzleDb.schemas.agent)
|
.update(drizzleDb.schemas.agent)
|
||||||
.set(withUpdatedAt({
|
.set(
|
||||||
|
withUpdatedAt({
|
||||||
version: body.version,
|
version: body.version,
|
||||||
lastContact: lastContact,
|
lastContact: lastContact,
|
||||||
healthErrorCount: null
|
healthErrorCount: null,
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.where(eq(drizzleDb.schemas.agent.id, agentId));
|
.where(eq(drizzleDb.schemas.agent.id, agentId));
|
||||||
|
|
||||||
await db
|
await db.insert(drizzleDb.schemas.healthcheckLog).values({
|
||||||
.insert(drizzleDb.schemas.healthcheckLog)
|
|
||||||
.values({
|
|
||||||
kind: "agent",
|
kind: "agent",
|
||||||
status: "success",
|
status: "success",
|
||||||
objectId: agentId,
|
objectId: agentId,
|
||||||
date: lastContact
|
date: lastContact,
|
||||||
})
|
});
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
agent: {
|
agent: {
|
||||||
id: agentId,
|
id: agentId,
|
||||||
lastContact: lastContact
|
lastContact: lastContact,
|
||||||
},
|
},
|
||||||
databases: databasesResponse
|
databases: databasesResponse,
|
||||||
}
|
};
|
||||||
|
|
||||||
return Response.json(response)
|
return Response.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({error: error}, "Error in POST handler")
|
log.error({ error: error }, "Error in POST handler");
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: 'Internal server error'},
|
{ error: "Internal server error" },
|
||||||
{status: 500}
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { headers } from "next/headers";
|
|||||||
|
|
||||||
const authHandler = toNextJsHandler(auth.handler);
|
const authHandler = toNextJsHandler(auth.handler);
|
||||||
|
|
||||||
async function blockApiKeyCreateForRestrictedUsers(req: NextRequest): Promise<NextResponse | null> {
|
async function blockApiKeyCreateForRestrictedUsers(
|
||||||
|
req: NextRequest,
|
||||||
|
): Promise<NextResponse | null> {
|
||||||
const url = req.nextUrl;
|
const url = req.nextUrl;
|
||||||
if (req.method !== "POST" || !url.pathname.endsWith("/api-key/create")) {
|
if (req.method !== "POST" || !url.pathname.endsWith("/api-key/create")) {
|
||||||
return null;
|
return null;
|
||||||
@@ -18,7 +20,7 @@ async function blockApiKeyCreateForRestrictedUsers(req: NextRequest): Promise<Ne
|
|||||||
if (session.user.banned || (session.user.role as string) === "pending") {
|
if (session.user.banned || (session.user.role as string) === "pending") {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Account not eligible to create API keys" },
|
{ error: "Account not eligible to create API keys" },
|
||||||
{ status: 403 }
|
{ status: 403 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import type {StorageInput} from "@/features/storages/storages.types";
|
import type {StorageInput} from "@/features/storages/types";
|
||||||
import {dispatchStorage} from "@/features/storages/storages.dispatch";
|
import {dispatchStorage} from "@/features/storages/utils/storages.dispatch";
|
||||||
import {Readable} from "node:stream";
|
import {Readable} from "node:stream";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {auth} from "@/lib/auth/auth";
|
import {auth} from "@/lib/auth/auth";
|
||||||
import {headers} from "next/headers";
|
import {headers} from "next/headers";
|
||||||
import {StorageInput} from "@/features/storages/storages.types";
|
import {StorageInput} from "@/features/storages/types";
|
||||||
import {dispatchStorage} from "@/features/storages/storages.dispatch";
|
import {dispatchStorage} from "@/features/storages/utils/storages.dispatch";
|
||||||
import {Readable} from "node:stream";
|
import {Readable} from "node:stream";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {withApiKey} from "@/lib/api-v1/middleware";
|
|||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
import {ApiKeyContext} from "@/lib/api-v1/types";
|
import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||||
import {getAgent, resolveAgentAccess} from "@/lib/api-v1/services/agents";
|
import {getAgent, resolveAgentAccess} from "@/lib/api-v1/services/agents";
|
||||||
import {deleteAgentService} from "@/features/agents/agent-delete.action";
|
import {deleteAgentService} from "@/features/agents/actions/agent-delete.action";
|
||||||
|
|
||||||
const log = logger.child({module: "api/v1/agents/[id]"});
|
const log = logger.child({module: "api/v1/agents/[id]"});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as drizzleDb from "@/db";
|
|||||||
import { inArray, eq, and, or, isNull } from "drizzle-orm";
|
import { inArray, eq, and, or, isNull } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import {createAgentService} from "@/features/agents/agents.action";
|
import {createAgentService} from "@/features/agents/actions/agents.action";
|
||||||
import { ActionError } from "@/lib/safe-actions/actions";
|
import { ActionError } from "@/lib/safe-actions/actions";
|
||||||
import {getAccessibleAgentIds} from "@/lib/api-v1/services/agents";
|
import {getAccessibleAgentIds} from "@/lib/api-v1/services/agents";
|
||||||
import {ApiKeyContext} from "@/lib/api-v1/types";
|
import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||||
|
|||||||
+5
-2
@@ -1,4 +1,5 @@
|
|||||||
import BackButton from "@/components/common/back-button";
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
export default async function NotFound() {
|
export default async function NotFound() {
|
||||||
return (
|
return (
|
||||||
@@ -12,7 +13,9 @@ export default async function NotFound() {
|
|||||||
The content you are trying to view is not available.
|
The content you are trying to view is not available.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<BackButton>Go home</BackButton>
|
<Button asChild>
|
||||||
|
<Link href="/">Go home</Link>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+3
-3
@@ -5,8 +5,8 @@ import { type PropsWithChildren, Suspense } from "react";
|
|||||||
|
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { ErrorLayout } from "@/components/common/error-layout";
|
import { ErrorLayout } from "@/components/common/error-layout";
|
||||||
import { ThemeMetaUpdaterRoot } from "@/features/theme/theme-meta-updater-root";
|
import { ThemeMetaUpdater } from "@/features/theme/components/theme-meta-updater";
|
||||||
import { ThemeProvider } from "@/features/theme/theme-provider";
|
import { ThemeProvider } from "@/features/theme/components/theme-provider";
|
||||||
|
|
||||||
export type ProviderProps = PropsWithChildren<{}>;
|
export type ProviderProps = PropsWithChildren<{}>;
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -15,7 +15,7 @@ export const Providers = (props: ProviderProps) => {
|
|||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
<ThemeMetaUpdaterRoot />
|
<ThemeMetaUpdater />
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ErrorLayout>
|
<ErrorLayout>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
# build:
|
# build:
|
||||||
# context: .
|
# context: .
|
||||||
# dockerfile: docker/dockerfile/Dockerfile
|
# dockerfile: docker/dockerfile/Dockerfile
|
||||||
# target: prod
|
# target: prod
|
||||||
image: portabase/portabase:1
|
image: portabase/portabase:1
|
||||||
ports:
|
ports:
|
||||||
- '8887:80'
|
- "8887:80"
|
||||||
environment:
|
environment:
|
||||||
TZ: "Europe/Paris"
|
TZ: "Europe/Paris"
|
||||||
env_file:
|
env_file:
|
||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
- POSTGRES_USER=devuser
|
- POSTGRES_USER=devuser
|
||||||
- POSTGRES_PASSWORD=changeme
|
- POSTGRES_PASSWORD=changeme
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "pg_isready -U devuser -d devdb" ]
|
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|||||||
+14
-16
@@ -1,11 +1,10 @@
|
|||||||
import type {NextConfig} from "next";
|
import type { NextConfig } from "next";
|
||||||
import {PORTABASE_DEFAULT_SETTINGS} from "./portabase.config";
|
import { PORTABASE_DEFAULT_SETTINGS } from "./portabase.config";
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === "development";
|
const isDev = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
|
|
||||||
function buildCSPHeader(): string {
|
function buildCSPHeader(): string {
|
||||||
const {CSP} = PORTABASE_DEFAULT_SETTINGS.SECURITY;
|
const { CSP } = PORTABASE_DEFAULT_SETTINGS.SECURITY;
|
||||||
|
|
||||||
const directives = [
|
const directives = [
|
||||||
`default-src ${CSP.DEFAULT_SRC.join(" ")}`,
|
`default-src ${CSP.DEFAULT_SRC.join(" ")}`,
|
||||||
@@ -37,9 +36,9 @@ function buildPermissionsPolicy(): string {
|
|||||||
.join(", ");
|
.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
devIndicators: false,
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
@@ -50,7 +49,7 @@ const nextConfig: NextConfig = {
|
|||||||
serverActions: {
|
serverActions: {
|
||||||
bodySizeLimit: "10gb",
|
bodySizeLimit: "10gb",
|
||||||
},
|
},
|
||||||
proxyClientMaxBodySize: '10gb',
|
proxyClientMaxBodySize: "10gb",
|
||||||
},
|
},
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
if (!isDev) return [];
|
if (!isDev) return [];
|
||||||
@@ -77,22 +76,21 @@ const nextConfig: NextConfig = {
|
|||||||
value: buildPermissionsPolicy(),
|
value: buildPermissionsPolicy(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'X-Content-Type-Options',
|
key: "X-Content-Type-Options",
|
||||||
value: 'nosniff',
|
value: "nosniff",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'X-Frame-Options',
|
key: "X-Frame-Options",
|
||||||
value: 'DENY',
|
value: "DENY",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Referrer-Policy',
|
key: "Referrer-Policy",
|
||||||
value: 'strict-origin-when-cross-origin',
|
value: "strict-origin-when-cross-origin",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Strict-Transport-Security',
|
key: "Strict-Transport-Security",
|
||||||
value: 'max-age=63072000; includeSubDomains; preload',
|
value: "max-age=63072000; includeSubDomains; preload",
|
||||||
}
|
},
|
||||||
// ...other security headers
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"@better-auth/sso": "1.6.11",
|
"@better-auth/sso": "1.6.11",
|
||||||
"@hookform/resolvers": "^5.4.0",
|
"@hookform/resolvers": "^5.4.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
|
"@onboardjs/react": "1.0.0-rc.5",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
||||||
@@ -61,6 +62,7 @@
|
|||||||
"argon2": "^0.43.1",
|
"argon2": "^0.43.1",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"better-auth": "1.6.11",
|
"better-auth": "1.6.11",
|
||||||
|
"canvas-confetti": "^1.9.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -115,6 +117,7 @@
|
|||||||
"@release-it/bumper": "^7.0.5",
|
"@release-it/bumper": "^7.0.5",
|
||||||
"@release-it/conventional-changelog": "^10.0.6",
|
"@release-it/conventional-changelog": "^10.0.6",
|
||||||
"@tailwindcss/postcss": "^4.3.0",
|
"@tailwindcss/postcss": "^4.3.0",
|
||||||
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||||
"@types/node": "^22.19.19",
|
"@types/node": "^22.19.19",
|
||||||
"@types/node-forge": "^1.3.14",
|
"@types/node-forge": "^1.3.14",
|
||||||
|
|||||||
Generated
+56
-2
@@ -32,6 +32,9 @@ importers:
|
|||||||
'@modelcontextprotocol/sdk':
|
'@modelcontextprotocol/sdk':
|
||||||
specifier: ^1.29.0
|
specifier: ^1.29.0
|
||||||
version: 1.29.0(zod@4.3.6)
|
version: 1.29.0(zod@4.3.6)
|
||||||
|
'@onboardjs/react':
|
||||||
|
specifier: 1.0.0-rc.5
|
||||||
|
version: 1.0.0-rc.5(@onboardjs/core@1.0.0-rc.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-accordion':
|
'@radix-ui/react-accordion':
|
||||||
specifier: ^1.2.12
|
specifier: ^1.2.12
|
||||||
version: 1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
@@ -146,6 +149,9 @@ importers:
|
|||||||
better-auth:
|
better-auth:
|
||||||
specifier: 1.6.11
|
specifier: 1.6.11
|
||||||
version: 1.6.11(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.21.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(@playwright/test@1.58.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.6.11(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.21.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(@playwright/test@1.58.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
|
canvas-confetti:
|
||||||
|
specifier: ^1.9.4
|
||||||
|
version: 1.9.4
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -303,6 +309,9 @@ importers:
|
|||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.3.0
|
specifier: ^4.3.0
|
||||||
version: 4.3.0
|
version: 4.3.0
|
||||||
|
'@types/canvas-confetti':
|
||||||
|
specifier: ^1.9.0
|
||||||
|
version: 1.9.0
|
||||||
'@types/eslint-plugin-tailwindcss':
|
'@types/eslint-plugin-tailwindcss':
|
||||||
specifier: ^3.17.0
|
specifier: ^3.17.0
|
||||||
version: 3.17.0
|
version: 3.17.0
|
||||||
@@ -2237,6 +2246,16 @@ packages:
|
|||||||
'@octokit/types@16.0.0':
|
'@octokit/types@16.0.0':
|
||||||
resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==}
|
resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==}
|
||||||
|
|
||||||
|
'@onboardjs/core@1.0.0-rc.4':
|
||||||
|
resolution: {integrity: sha512-G22h1I2JUiEyDGf06YEUT2Afg26rDZM4Ku0PAZFqmr7HVukg9651UdPxZ1/9TVvAFFmW6Qq+u8oQ75KVJu80pw==}
|
||||||
|
|
||||||
|
'@onboardjs/react@1.0.0-rc.5':
|
||||||
|
resolution: {integrity: sha512-iqC4MjrQe4VLi3HKCkczFigkjLxdjWTYrzoI1jEi3QpKOVmDAn4bmXngazW2nfPSOItkxdQq38VO7q8J2yVMhg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@onboardjs/core': '>=1.0.0-rc.1'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
'@opentelemetry/semantic-conventions@1.41.1':
|
'@opentelemetry/semantic-conventions@1.41.1':
|
||||||
resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==}
|
resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -3849,6 +3868,9 @@ packages:
|
|||||||
'@tybys/wasm-util@0.10.2':
|
'@tybys/wasm-util@0.10.2':
|
||||||
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
|
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
|
||||||
|
|
||||||
|
'@types/canvas-confetti@1.9.0':
|
||||||
|
resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==}
|
||||||
|
|
||||||
'@types/cors@2.8.19':
|
'@types/cors@2.8.19':
|
||||||
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
|
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
|
||||||
|
|
||||||
@@ -4656,6 +4678,9 @@ packages:
|
|||||||
caniuse-lite@1.0.30001797:
|
caniuse-lite@1.0.30001797:
|
||||||
resolution: {integrity: sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==}
|
resolution: {integrity: sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==}
|
||||||
|
|
||||||
|
canvas-confetti@1.9.4:
|
||||||
|
resolution: {integrity: sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==}
|
||||||
|
|
||||||
capital-case@1.0.4:
|
capital-case@1.0.4:
|
||||||
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
|
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
|
||||||
|
|
||||||
@@ -6997,6 +7022,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
p-queue@8.1.1:
|
||||||
|
resolution: {integrity: sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
p-timeout@6.1.4:
|
||||||
|
resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==}
|
||||||
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
pac-proxy-agent@7.2.0:
|
pac-proxy-agent@7.2.0:
|
||||||
resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==}
|
resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
@@ -10062,6 +10095,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@octokit/openapi-types': 27.0.0
|
'@octokit/openapi-types': 27.0.0
|
||||||
|
|
||||||
|
'@onboardjs/core@1.0.0-rc.4':
|
||||||
|
dependencies:
|
||||||
|
p-queue: 8.1.1
|
||||||
|
|
||||||
|
'@onboardjs/react@1.0.0-rc.5(@onboardjs/core@1.0.0-rc.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
|
dependencies:
|
||||||
|
'@onboardjs/core': 1.0.0-rc.4
|
||||||
|
react: 19.2.7
|
||||||
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
|
|
||||||
'@opentelemetry/semantic-conventions@1.41.1': {}
|
'@opentelemetry/semantic-conventions@1.41.1': {}
|
||||||
|
|
||||||
'@paralleldrive/cuid2@2.3.1':
|
'@paralleldrive/cuid2@2.3.1':
|
||||||
@@ -11837,6 +11880,8 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@types/canvas-confetti@1.9.0': {}
|
||||||
|
|
||||||
'@types/cors@2.8.19':
|
'@types/cors@2.8.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.19.20
|
'@types/node': 22.19.20
|
||||||
@@ -12793,6 +12838,8 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001797: {}
|
caniuse-lite@1.0.30001797: {}
|
||||||
|
|
||||||
|
canvas-confetti@1.9.4: {}
|
||||||
|
|
||||||
capital-case@1.0.4:
|
capital-case@1.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
no-case: 3.0.4
|
no-case: 3.0.4
|
||||||
@@ -15359,6 +15406,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-limit: 3.1.0
|
p-limit: 3.1.0
|
||||||
|
|
||||||
|
p-queue@8.1.1:
|
||||||
|
dependencies:
|
||||||
|
eventemitter3: 5.0.4
|
||||||
|
p-timeout: 6.1.4
|
||||||
|
|
||||||
|
p-timeout@6.1.4: {}
|
||||||
|
|
||||||
pac-proxy-agent@7.2.0:
|
pac-proxy-agent@7.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tootallnate/quickjs-emscripten': 0.23.0
|
'@tootallnate/quickjs-emscripten': 0.23.0
|
||||||
@@ -16657,7 +16711,7 @@ snapshots:
|
|||||||
ansi-escapes: 4.3.2
|
ansi-escapes: 4.3.2
|
||||||
supports-hyperlinks: 2.3.0
|
supports-hyperlinks: 2.3.0
|
||||||
|
|
||||||
terser-webpack-plugin@5.6.1(esbuild@0.25.10)(postcss@8.5.10)(webpack@5.107.2(esbuild@0.27.7)(postcss@8.5.10)):
|
terser-webpack-plugin@5.6.1(esbuild@0.25.10)(postcss@8.5.10)(webpack@5.107.2(esbuild@0.25.10)(postcss@8.5.10)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
jest-worker: 27.5.1
|
jest-worker: 27.5.1
|
||||||
@@ -17082,7 +17136,7 @@ snapshots:
|
|||||||
neo-async: 2.6.2
|
neo-async: 2.6.2
|
||||||
schema-utils: 4.3.3
|
schema-utils: 4.3.3
|
||||||
tapable: 2.3.3
|
tapable: 2.3.3
|
||||||
terser-webpack-plugin: 5.6.1(esbuild@0.25.10)(postcss@8.5.10)(webpack@5.107.2(esbuild@0.27.7)(postcss@8.5.10))
|
terser-webpack-plugin: 5.6.1(esbuild@0.25.10)(postcss@8.5.10)(webpack@5.107.2(esbuild@0.25.10)(postcss@8.5.10))
|
||||||
watchpack: 2.5.1
|
watchpack: 2.5.1
|
||||||
webpack-sources: 3.5.0
|
webpack-sources: 3.5.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|||||||
+1
-1
@@ -46,7 +46,7 @@ export const PORTABASE_DEFAULT_SETTINGS = {
|
|||||||
"https://api.iconify.design",
|
"https://api.iconify.design",
|
||||||
"https://code.iconify.design",
|
"https://code.iconify.design",
|
||||||
"https://api.github.com",
|
"https://api.github.com",
|
||||||
|
"https://api.dicebear.com",
|
||||||
],
|
],
|
||||||
OBJECT_SRC: ["'none'"],
|
OBJECT_SRC: ["'none'"],
|
||||||
BASE_URI: ["'self'"],
|
BASE_URI: ["'self'"],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { errorHandler } from "@/middleware/errorHandler";
|
|||||||
import { auth } from "@/lib/auth/auth";
|
import { auth } from "@/lib/auth/auth";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { env } from "@/env.mjs";
|
import { env } from "@/env.mjs";
|
||||||
import {User} from "@/db/schema/02_user";
|
import { User } from "@/db/schema/02_user";
|
||||||
|
|
||||||
export async function proxy(request: NextRequest) {
|
export async function proxy(request: NextRequest) {
|
||||||
const url = request.nextUrl.clone();
|
const url = request.nextUrl.clone();
|
||||||
@@ -19,7 +19,7 @@ export async function proxy(request: NextRequest) {
|
|||||||
new URL(`/login?redirect=${redirectUrl}`, request.url),
|
new URL(`/login?redirect=${redirectUrl}`, request.url),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const user = session.user as User
|
const user = session.user as User;
|
||||||
|
|
||||||
if (user.banned) {
|
if (user.banned) {
|
||||||
await auth.api.signOut({ headers: await headers() });
|
await auth.api.signOut({ headers: await headers() });
|
||||||
@@ -104,6 +104,7 @@ function checkRouteExists(pathname: string) {
|
|||||||
/^\/api\/config\/?$/,
|
/^\/api\/config\/?$/,
|
||||||
/^\/api\/health\/?$/,
|
/^\/api\/health\/?$/,
|
||||||
/^\/api\/google\/drive\/callback\/?$/,
|
/^\/api\/google\/drive\/callback\/?$/,
|
||||||
|
/^\/api\/avatar\/?$/,
|
||||||
// v1 external API
|
// v1 external API
|
||||||
/^\/api\/v1\/mcp\/?$/,
|
/^\/api\/v1\/mcp\/?$/,
|
||||||
/^\/api\/v1\/docs\/?$/,
|
/^\/api\/v1\/docs\/?$/,
|
||||||
|
|||||||
@@ -351,6 +351,14 @@
|
|||||||
"clientRole" : true,
|
"clientRole" : true,
|
||||||
"containerId" : "18b5dfb2-44d4-448e-98df-9652daef1a22",
|
"containerId" : "18b5dfb2-44d4-448e-98df-9652daef1a22",
|
||||||
"attributes" : { }
|
"attributes" : { }
|
||||||
|
} ],
|
||||||
|
"exemple-client" : [ {
|
||||||
|
"id" : "67766209-57dd-45c7-940d-ed3313473101",
|
||||||
|
"name" : "uma_protection",
|
||||||
|
"composite" : false,
|
||||||
|
"clientRole" : true,
|
||||||
|
"containerId" : "248d3eaa-466f-4b85-8903-9242959e67ba",
|
||||||
|
"attributes" : { }
|
||||||
} ]
|
} ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -458,8 +466,8 @@
|
|||||||
"id" : "b1cedcac-5207-4479-bc29-065ba849e414",
|
"id" : "b1cedcac-5207-4479-bc29-065ba849e414",
|
||||||
"type" : "password",
|
"type" : "password",
|
||||||
"userLabel" : "My password",
|
"userLabel" : "My password",
|
||||||
"createdDate" : 1772179391727,
|
"createdDate" : 1782393421785,
|
||||||
"secretData" : "{\"value\":\"IXr6L3pvO2BAkn/cH3rh3v2tg1R83u2f8SWEZK2fq5E=\",\"salt\":\"XWV8/yKm7Rw/YXiwXZZUig==\",\"additionalParameters\":{}}",
|
"secretData" : "{\"value\":\"L585pLqSeHaNW4HzOu+GzZm+VJWWBO3Df+rPs04gUGk=\",\"salt\":\"w3pmQ0hsMDJnCX+qJ3K+zQ==\",\"additionalParameters\":{}}",
|
||||||
"credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
|
"credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
|
||||||
} ],
|
} ],
|
||||||
"disableableCredentialTypes" : [ ],
|
"disableableCredentialTypes" : [ ],
|
||||||
@@ -467,6 +475,23 @@
|
|||||||
"realmRoles" : [ "admin", "default-roles-master" ],
|
"realmRoles" : [ "admin", "default-roles-master" ],
|
||||||
"notBefore" : 0,
|
"notBefore" : 0,
|
||||||
"groups" : [ "/Admin" ]
|
"groups" : [ "/Admin" ]
|
||||||
|
}, {
|
||||||
|
"id" : "8c749a0c-f2ed-4bc7-9f54-4e0bcbc36626",
|
||||||
|
"username" : "service-account-exemple-client",
|
||||||
|
"emailVerified" : false,
|
||||||
|
"enabled" : true,
|
||||||
|
"createdTimestamp" : 1772190246795,
|
||||||
|
"totp" : false,
|
||||||
|
"serviceAccountClientId" : "exemple-client",
|
||||||
|
"credentials" : [ ],
|
||||||
|
"disableableCredentialTypes" : [ ],
|
||||||
|
"requiredActions" : [ ],
|
||||||
|
"realmRoles" : [ "default-roles-master" ],
|
||||||
|
"clientRoles" : {
|
||||||
|
"exemple-client" : [ "uma_protection" ]
|
||||||
|
},
|
||||||
|
"notBefore" : 0,
|
||||||
|
"groups" : [ ]
|
||||||
}, {
|
}, {
|
||||||
"id" : "718dfe46-dbef-41a3-9120-78aeac240431",
|
"id" : "718dfe46-dbef-41a3-9120-78aeac240431",
|
||||||
"username" : "service-account-portabase",
|
"username" : "service-account-portabase",
|
||||||
@@ -625,6 +650,57 @@
|
|||||||
"nodeReRegistrationTimeout" : 0,
|
"nodeReRegistrationTimeout" : 0,
|
||||||
"defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ],
|
"defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ],
|
||||||
"optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ]
|
"optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ]
|
||||||
|
}, {
|
||||||
|
"id" : "248d3eaa-466f-4b85-8903-9242959e67ba",
|
||||||
|
"clientId" : "exemple-client",
|
||||||
|
"name" : "Exemple Client",
|
||||||
|
"description" : "",
|
||||||
|
"rootUrl" : "https://your-instance.portabase.io",
|
||||||
|
"adminUrl" : "https://your-instance.portabase.io",
|
||||||
|
"baseUrl" : "https://your-instance.portabase.io",
|
||||||
|
"surrogateAuthRequired" : false,
|
||||||
|
"enabled" : true,
|
||||||
|
"alwaysDisplayInConsole" : false,
|
||||||
|
"clientAuthenticatorType" : "client-secret",
|
||||||
|
"secret" : "lFZY6fXzh9uxCXwvucw9JtXGhNRKgnnj",
|
||||||
|
"redirectUris" : [ "https://your-instance.portabase.io/api/auth/sso/callback/exemple-client" ],
|
||||||
|
"webOrigins" : [ "https://your-instance.portabase.io" ],
|
||||||
|
"notBefore" : 0,
|
||||||
|
"bearerOnly" : false,
|
||||||
|
"consentRequired" : false,
|
||||||
|
"standardFlowEnabled" : true,
|
||||||
|
"implicitFlowEnabled" : false,
|
||||||
|
"directAccessGrantsEnabled" : false,
|
||||||
|
"serviceAccountsEnabled" : true,
|
||||||
|
"authorizationServicesEnabled" : true,
|
||||||
|
"publicClient" : false,
|
||||||
|
"frontchannelLogout" : true,
|
||||||
|
"protocol" : "openid-connect",
|
||||||
|
"attributes" : {
|
||||||
|
"realm_client" : "false",
|
||||||
|
"oidc.ciba.grant.enabled" : "false",
|
||||||
|
"client.secret.creation.time" : "1772190246",
|
||||||
|
"backchannel.logout.session.required" : "true",
|
||||||
|
"standard.token.exchange.enabled" : "false",
|
||||||
|
"post.logout.redirect.uris" : "https://your-instance.portabase.io",
|
||||||
|
"oauth2.device.authorization.grant.enabled" : "true",
|
||||||
|
"pkce.code.challenge.method" : "S256",
|
||||||
|
"backchannel.logout.revoke.offline.tokens" : "false",
|
||||||
|
"dpop.bound.access.tokens" : "false"
|
||||||
|
},
|
||||||
|
"authenticationFlowBindingOverrides" : { },
|
||||||
|
"fullScopeAllowed" : true,
|
||||||
|
"nodeReRegistrationTimeout" : -1,
|
||||||
|
"defaultClientScopes" : [ "web-origins", "service_account", "acr", "profile", "roles", "basic", "email" ],
|
||||||
|
"optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ],
|
||||||
|
"authorizationSettings" : {
|
||||||
|
"allowRemoteResourceManagement" : true,
|
||||||
|
"policyEnforcementMode" : "ENFORCING",
|
||||||
|
"resources" : [ ],
|
||||||
|
"policies" : [ ],
|
||||||
|
"scopes" : [ ],
|
||||||
|
"decisionStrategy" : "UNANIMOUS"
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
"id" : "6022c206-5f1f-45b7-af07-74ce6804cbc7",
|
"id" : "6022c206-5f1f-45b7-af07-74ce6804cbc7",
|
||||||
"clientId" : "master-realm",
|
"clientId" : "master-realm",
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
type BackButtonProps = React.ComponentProps<typeof Button> & {
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function BackButton({ children, ...props }: BackButtonProps) {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button onClick={() => router.back()} aria-label={children?.toString()} {...props}>
|
|
||||||
{children}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export type ThemeKey = "dark" | "light" | "system";
|
||||||
|
|
||||||
|
const themes: { value: ThemeKey }[] = [
|
||||||
|
{ value: "light" },
|
||||||
|
{ value: "dark" },
|
||||||
|
{ value: "system" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const THEME_TEXT: Record<ThemeKey, string> = {
|
||||||
|
dark: "Dark",
|
||||||
|
light: "Light",
|
||||||
|
system: "System",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ThemeSelectorProps {
|
||||||
|
value?: string;
|
||||||
|
onSelect: (value: ThemeKey) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeSelector({
|
||||||
|
value,
|
||||||
|
onSelect,
|
||||||
|
className,
|
||||||
|
}: ThemeSelectorProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("grid grid-cols-1 sm:grid-cols-3 gap-4 w-full", className)}
|
||||||
|
>
|
||||||
|
{themes.map((item) => {
|
||||||
|
const isDark = item.value === "dark";
|
||||||
|
const isSystem = item.value === "system";
|
||||||
|
const isActive = value === item.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.value}
|
||||||
|
className={cn(
|
||||||
|
"border-2 rounded-xl p-1 cursor-pointer transition-all hover:bg-accent/50 space-y-2",
|
||||||
|
isActive ? "border-primary bg-primary/5" : "border-muted/40",
|
||||||
|
)}
|
||||||
|
onClick={() => onSelect(item.value)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"p-2 rounded-lg aspect-4/3 flex flex-col gap-2 relative overflow-hidden border",
|
||||||
|
isDark
|
||||||
|
? "bg-slate-950 border-slate-800"
|
||||||
|
: "bg-white border-slate-200",
|
||||||
|
isSystem && "bg-linear-to-br from-white to-slate-950",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-3 w-full rounded-sm shadow-sm opacity-80",
|
||||||
|
isDark ? "bg-slate-800" : "bg-slate-100",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2 flex-1 relative">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-1/4 h-full rounded-sm shadow-sm opacity-80",
|
||||||
|
isDark ? "bg-slate-800" : "bg-slate-100",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex-1 flex flex-col gap-2">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-3 w-full rounded-sm shadow-sm opacity-80",
|
||||||
|
isDark ? "bg-slate-800" : "bg-slate-100",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex-1 rounded-sm shadow-sm p-1 space-y-2 opacity-50",
|
||||||
|
isDark ? "bg-slate-800" : "bg-slate-100",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-2 w-3/4 rounded-full",
|
||||||
|
isDark ? "bg-slate-700" : "bg-slate-300",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-2 w-full rounded-full",
|
||||||
|
isDark ? "bg-slate-700" : "bg-slate-300",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-1 px-2">
|
||||||
|
<span className="font-medium text-sm">
|
||||||
|
{THEME_TEXT[item.value]}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-4 h-4 rounded-full border flex items-center justify-center transition-all",
|
||||||
|
isActive
|
||||||
|
? "border-primary bg-primary"
|
||||||
|
: "border-muted-foreground/30",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isActive && (
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-primary-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// import * as React from "react";
|
// import * as React from "react";
|
||||||
// import EmailLayout from "./email-layout";
|
// import EmailLayout from "./email-layout";
|
||||||
// import {Text, Section, Button} from "@react-email/components";
|
// import {Text, Section, Button} from "@react-email/components";
|
||||||
// import type {EventPayload} from "@/features/notifications/notifications.types";
|
// import type {EventPayload} from "@/features/notifications/types";
|
||||||
//
|
//
|
||||||
// export interface EmailNotificationProps {
|
// export interface EmailNotificationProps {
|
||||||
// payload: EventPayload
|
// payload: EventPayload
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import EmailLayout from "./email-layout";
|
import EmailLayout from "./email-layout";
|
||||||
import {Text, Section, Button} from "@react-email/components";
|
import {Text, Section, Button} from "@react-email/components";
|
||||||
import type {EventPayload} from "@/features/notifications/notifications.types";
|
import type {EventPayload} from "@/features/notifications/types";
|
||||||
|
|
||||||
export interface EmailNotificationProps {
|
export interface EmailNotificationProps {
|
||||||
payload: EventPayload;
|
payload: EventPayload;
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "settings" ADD COLUMN "onboarding" boolean DEFAULT false NOT NULL;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- Custom SQL migration file, put your code below! --
|
||||||
|
UPDATE settings SET onboarding = true
|
||||||
|
WHERE onboarding = false
|
||||||
|
AND (
|
||||||
|
EXISTS (SELECT 1 FROM organization WHERE slug <> 'default')
|
||||||
|
OR EXISTS (SELECT 1 FROM agents)
|
||||||
|
OR EXISTS (SELECT 1 FROM projects)
|
||||||
|
OR EXISTS (SELECT 1 FROM databases)
|
||||||
|
OR EXISTS (SELECT 1 FROM notification_channel)
|
||||||
|
OR EXISTS (SELECT 1 FROM storage_channel WHERE provider <> 'local')
|
||||||
|
);
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -456,6 +456,20 @@
|
|||||||
"when": 1781960184847,
|
"when": 1781960184847,
|
||||||
"tag": "0064_orange_richard_fisk",
|
"tag": "0064_orange_richard_fisk",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 65,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782474059918,
|
||||||
|
"tag": "0065_mighty_killer_shrike",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 66,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782474105293,
|
||||||
|
"tag": "0066_milky_puma",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -20,10 +20,11 @@ export const setting = pgTable("settings", {
|
|||||||
defaultStorageChannelId: uuid('default_storage_channel_id')
|
defaultStorageChannelId: uuid('default_storage_channel_id')
|
||||||
.references(() => storageChannel.id, {onDelete: "set null"}),
|
.references(() => storageChannel.id, {onDelete: "set null"}),
|
||||||
encryption: boolean("encryption").default(false),
|
encryption: boolean("encryption").default(false),
|
||||||
|
onboarding: boolean("onboarding").default(false).notNull(),
|
||||||
...timestamps
|
...timestamps
|
||||||
});
|
});
|
||||||
|
|
||||||
export const settingRelations = relations(setting, ({one, many}) => ({
|
export const settingRelations = relations(setting, ({one}) => ({
|
||||||
storageChannel: one(storageChannel, {fields: [setting.defaultStorageChannelId], references: [storageChannel.id]}),
|
storageChannel: one(storageChannel, {fields: [setting.defaultStorageChannelId], references: [storageChannel.id]}),
|
||||||
notificationChannel: one(notificationChannel, {fields: [setting.defaultNotificationChannelId], references: [notificationChannel.id]}),
|
notificationChannel: one(notificationChannel, {fields: [setting.defaultNotificationChannelId], references: [notificationChannel.id]}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
+11
-13
@@ -1,11 +1,12 @@
|
|||||||
import {and, desc, eq, sql} from "drizzle-orm";
|
"use server";
|
||||||
import {db} from "@/db";
|
|
||||||
import {Agent, agent, organizationAgent} from "@/db/schema/08_agent";
|
import { and, desc, eq, sql } from "drizzle-orm";
|
||||||
import {Database, database} from "@/db/schema/07_database";
|
import { db } from "@/db";
|
||||||
|
import { Agent, agent, organizationAgent } from "@/db/schema/08_agent";
|
||||||
|
import { Database, database } from "@/db/schema/07_database";
|
||||||
|
|
||||||
export async function getOrganizationAgents(organizationId: string) {
|
export async function getOrganizationAgents(organizationId: string) {
|
||||||
|
return (await db
|
||||||
return await db
|
|
||||||
.select({
|
.select({
|
||||||
id: agent.id,
|
id: agent.id,
|
||||||
name: agent.name,
|
name: agent.name,
|
||||||
@@ -27,17 +28,14 @@ export async function getOrganizationAgents(organizationId: string) {
|
|||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
.from(organizationAgent)
|
.from(organizationAgent)
|
||||||
.innerJoin(
|
.innerJoin(agent, eq(organizationAgent.agentId, agent.id))
|
||||||
agent,
|
|
||||||
eq(organizationAgent.agentId, agent.id)
|
|
||||||
)
|
|
||||||
.leftJoin(database, eq(database.agentId, agent.id))
|
.leftJoin(database, eq(database.agentId, agent.id))
|
||||||
.groupBy(agent.id)
|
.groupBy(agent.id)
|
||||||
.orderBy(desc(agent.createdAt))
|
.orderBy(desc(agent.createdAt))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(organizationAgent.organizationId, organizationId),
|
eq(organizationAgent.organizationId, organizationId),
|
||||||
eq(agent.isArchived, false)
|
eq(agent.isArchived, false),
|
||||||
)
|
),
|
||||||
) as unknown as Agent[];
|
)) as unknown as Agent[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use server"
|
"use server";
|
||||||
import {eq} from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import { db } from "@/db";
|
||||||
|
|
||||||
export async function getDatabaseBackups(databaseId: string) {
|
export async function getDatabaseBackups(databaseId: string) {
|
||||||
return await db.query.backup.findMany({
|
return await db.query.backup.findMany({
|
||||||
@@ -14,6 +14,6 @@ export async function getDatabaseBackups(databaseId: string) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: (b, {desc}) => [desc(b.createdAt)],
|
orderBy: (b, { desc }) => [desc(b.createdAt)],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-16
@@ -1,15 +1,21 @@
|
|||||||
"use server"
|
"use server";
|
||||||
import {db} from "@/db";
|
import { inArray } from "drizzle-orm";
|
||||||
import {DatabaseWith} from "@/db/schema/07_database";
|
import { db } from "@/db";
|
||||||
import {AgentWith} from "@/db/schema/08_agent";
|
import { database, retentionPolicy } from "@/db/schema/07_database";
|
||||||
|
import { alertPolicy } from "@/db/schema/10_alert-policy";
|
||||||
|
import { storagePolicy } from "@/db/schema/13_storage-policy";
|
||||||
|
import { DatabaseWith } from "@/db/schema/07_database";
|
||||||
|
import { AgentWith } from "@/db/schema/08_agent";
|
||||||
|
import type {
|
||||||
|
OnboardingDbSettings,
|
||||||
|
EventKind,
|
||||||
|
} from "@/features/onboarding/types";
|
||||||
|
|
||||||
export async function getOrganizationAvailableDatabases(
|
export async function getOrganizationAvailableDatabases(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
projectId?: string
|
projectId?: string,
|
||||||
) {
|
) {
|
||||||
|
const availableDatabases = (await db.query.database.findMany({
|
||||||
const availableDatabases = (
|
|
||||||
await db.query.database.findMany({
|
|
||||||
where: (db, { eq, or, isNull }) =>
|
where: (db, { eq, or, isNull }) =>
|
||||||
projectId
|
projectId
|
||||||
? or(isNull(db.projectId), eq(db.projectId, projectId))
|
? or(isNull(db.projectId), eq(db.projectId, projectId))
|
||||||
@@ -17,23 +23,101 @@ export async function getOrganizationAvailableDatabases(
|
|||||||
with: {
|
with: {
|
||||||
agent: {
|
agent: {
|
||||||
with: {
|
with: {
|
||||||
organizations: true
|
organizations: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
project: true,
|
project: true,
|
||||||
backups: true,
|
backups: true,
|
||||||
restorations: true,
|
restorations: true,
|
||||||
},
|
},
|
||||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
orderBy: (db, { desc }) => [desc(db.createdAt)],
|
||||||
})
|
})) as DatabaseWith[];
|
||||||
) as DatabaseWith[];
|
|
||||||
|
|
||||||
return availableDatabases.filter(db => {
|
return availableDatabases.filter((db) => {
|
||||||
const agent = db.agent as AgentWith;
|
const agent = db.agent as AgentWith;
|
||||||
if (agent?.isArchived) return false;
|
if (agent?.isArchived) return false;
|
||||||
return (
|
return (
|
||||||
agent?.organizationId === organizationId ||
|
agent?.organizationId === organizationId ||
|
||||||
agent?.organizations?.some(org => org.organizationId === organizationId)
|
agent?.organizations?.some((org) => org.organizationId === organizationId)
|
||||||
);
|
);
|
||||||
})
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDatabasesSettings(
|
||||||
|
databaseIds: string[],
|
||||||
|
): Promise<Record<string, OnboardingDbSettings>> {
|
||||||
|
if (databaseIds.length === 0) return {};
|
||||||
|
|
||||||
|
const [retentionPolicies, dbs, alertPolicies, storagePolicies] =
|
||||||
|
await Promise.all([
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(retentionPolicy)
|
||||||
|
.where(inArray(retentionPolicy.databaseId, databaseIds)),
|
||||||
|
db
|
||||||
|
.select({ id: database.id, backupPolicy: database.backupPolicy })
|
||||||
|
.from(database)
|
||||||
|
.where(inArray(database.id, databaseIds)),
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(alertPolicy)
|
||||||
|
.where(inArray(alertPolicy.databaseId, databaseIds)),
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(storagePolicy)
|
||||||
|
.where(inArray(storagePolicy.databaseId, databaseIds)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result: Record<string, OnboardingDbSettings> = {};
|
||||||
|
|
||||||
|
for (const dbId of databaseIds) {
|
||||||
|
const rp = retentionPolicies.find((r) => r.databaseId === dbId);
|
||||||
|
const dbRow = dbs.find((d) => d.id === dbId);
|
||||||
|
const alerts = alertPolicies.filter((a) => a.databaseId === dbId);
|
||||||
|
const storages = storagePolicies.filter((s) => s.databaseId === dbId);
|
||||||
|
|
||||||
|
const settings: OnboardingDbSettings = {};
|
||||||
|
|
||||||
|
if (rp) {
|
||||||
|
settings.retention = {
|
||||||
|
type: rp.type,
|
||||||
|
count: rp.count ?? 7,
|
||||||
|
days: rp.days ?? 30,
|
||||||
|
gfs: {
|
||||||
|
daily: rp.gfsDaily ?? 7,
|
||||||
|
weekly: rp.gfsWeekly ?? 4,
|
||||||
|
monthly: rp.gfsMonthly ?? 12,
|
||||||
|
yearly: rp.gfsYearly ?? 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbRow) {
|
||||||
|
if (dbRow.backupPolicy) {
|
||||||
|
settings.backupMethod = "automatic";
|
||||||
|
settings.backupCron = dbRow.backupPolicy;
|
||||||
|
} else {
|
||||||
|
settings.backupMethod = "manual";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alerts.length > 0) {
|
||||||
|
settings.notificationPolicies = alerts.map((a) => ({
|
||||||
|
channelId: a.notificationChannelId,
|
||||||
|
eventKinds: a.eventKinds as EventKind[],
|
||||||
|
enabled: a.enabled,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storages.length > 0) {
|
||||||
|
settings.storagePolicies = storages.map((s) => ({
|
||||||
|
channelId: s.storageChannelId,
|
||||||
|
enabled: s.enabled,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
result[dbId] = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import {db} from "@/db";
|
"use server";
|
||||||
|
|
||||||
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {and, eq, gte, isNotNull, lt} from "drizzle-orm";
|
import { and, eq, gte, isNotNull, lt } from "drizzle-orm";
|
||||||
import {dispatchNotification} from "@/features/notifications/notifications.dispatch";
|
import { dispatchNotification } from "@/features/notifications/utils/notifications.dispatch";
|
||||||
import {EventPayload} from "@/features/notifications/notifications.types";
|
import { EventPayload } from "@/features/notifications/types";
|
||||||
import {logger} from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
|
|
||||||
const log = logger.child({module: "tasks/healthcheck"});
|
const log = logger.child({ module: "tasks/healthcheck" });
|
||||||
|
|
||||||
export async function getHealthLast12hLogs({id}: { id: string }) {
|
export async function getHealthLast12hLogs({ id }: { id: string }) {
|
||||||
const now = new Date()
|
const now = new Date();
|
||||||
const since = new Date(now.getTime() - 12 * 60 * 60 * 1000)
|
const since = new Date(now.getTime() - 12 * 60 * 60 * 1000);
|
||||||
|
|
||||||
return db
|
return db
|
||||||
.select()
|
.select()
|
||||||
@@ -17,31 +19,30 @@ export async function getHealthLast12hLogs({id}: { id: string }) {
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(drizzleDb.schemas.healthcheckLog.objectId, id),
|
eq(drizzleDb.schemas.healthcheckLog.objectId, id),
|
||||||
gte(drizzleDb.schemas.healthcheckLog.date, since)
|
gte(drizzleDb.schemas.healthcheckLog.date, since),
|
||||||
)
|
),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteHealthLogsOlderThan12h() {
|
export async function deleteHealthLogsOlderThan12h() {
|
||||||
const now = new Date()
|
const now = new Date();
|
||||||
const threshold = new Date(now.getTime() - 12 * 60 * 60 * 1000)
|
const threshold = new Date(now.getTime() - 12 * 60 * 60 * 1000);
|
||||||
|
|
||||||
const logsToDelete = await db
|
const logsToDelete = await db
|
||||||
.select()
|
.select()
|
||||||
.from(drizzleDb.schemas.healthcheckLog)
|
.from(drizzleDb.schemas.healthcheckLog)
|
||||||
.where(
|
.where(lt(drizzleDb.schemas.healthcheckLog.date, threshold));
|
||||||
lt(drizzleDb.schemas.healthcheckLog.date, threshold)
|
|
||||||
)
|
|
||||||
|
|
||||||
log.info({name: "deleteHealthLogsOlderThan12h"},`Number of logs found to delete: ${logsToDelete.length}`)
|
log.info(
|
||||||
|
{ name: "deleteHealthLogsOlderThan12h" },
|
||||||
|
`Number of logs found to delete: ${logsToDelete.length}`,
|
||||||
|
);
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.delete(drizzleDb.schemas.healthcheckLog)
|
.delete(drizzleDb.schemas.healthcheckLog)
|
||||||
.where(
|
.where(lt(drizzleDb.schemas.healthcheckLog.date, threshold));
|
||||||
lt(drizzleDb.schemas.healthcheckLog.date, threshold)
|
|
||||||
)
|
|
||||||
|
|
||||||
return logsToDelete.length
|
return logsToDelete.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkAgentsHealthError() {
|
export async function checkAgentsHealthError() {
|
||||||
@@ -50,7 +51,7 @@ export async function checkAgentsHealthError() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const settings = await db.query.setting.findFirst({
|
const settings = await db.query.setting.findFirst({
|
||||||
where: (fields, {eq}) => eq(fields.name, "system"),
|
where: (fields, { eq }) => eq(fields.name, "system"),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
@@ -58,8 +59,11 @@ export async function checkAgentsHealthError() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!settings.defaultNotificationChannelId) {
|
if (!settings.defaultNotificationChannelId) {
|
||||||
log.error({name: "checkAgentsHealthError"},`No default notification channel id found.`)
|
log.error(
|
||||||
return
|
{ name: "checkAgentsHealthError" },
|
||||||
|
`No default notification channel id found.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -72,9 +76,9 @@ export async function checkAgentsHealthError() {
|
|||||||
|
|
||||||
if (diffMinutes > 10) {
|
if (diffMinutes > 10) {
|
||||||
if ((agent.healthErrorCount ?? 0) < 3) {
|
if ((agent.healthErrorCount ?? 0) < 3) {
|
||||||
|
const newHealthErrorCount = (agent.healthErrorCount ?? 0) + 1;
|
||||||
const newHealthErrorCount = (agent.healthErrorCount ?? 0) + 1
|
await db
|
||||||
await db.update(drizzleDb.schemas.agent)
|
.update(drizzleDb.schemas.agent)
|
||||||
.set({
|
.set({
|
||||||
healthErrorCount: newHealthErrorCount,
|
healthErrorCount: newHealthErrorCount,
|
||||||
})
|
})
|
||||||
@@ -91,31 +95,30 @@ export async function checkAgentsHealthError() {
|
|||||||
error: "Agent is down",
|
error: "Agent is down",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
log.info({name: "checkAgentsHealthError", payload: payload},`Agent Healthcheck Notification`)
|
log.info(
|
||||||
|
{ name: "checkAgentsHealthError", payload: payload },
|
||||||
|
`Agent Healthcheck Notification`,
|
||||||
|
);
|
||||||
|
|
||||||
await dispatchNotification(
|
await dispatchNotification(
|
||||||
payload,
|
payload,
|
||||||
undefined,
|
undefined,
|
||||||
settings.defaultNotificationChannelId,
|
settings.defaultNotificationChannelId,
|
||||||
undefined
|
undefined,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function checkDatabasesHealthError() {
|
export async function checkDatabasesHealthError() {
|
||||||
|
|
||||||
const databases = await db.query.database.findMany({
|
const databases = await db.query.database.findMany({
|
||||||
where: isNotNull(drizzleDb.schemas.database.lastContact),
|
where: isNotNull(drizzleDb.schemas.database.lastContact),
|
||||||
with: {
|
with: {
|
||||||
agent: true,
|
agent: true,
|
||||||
alertPolicies: true
|
alertPolicies: true,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
@@ -127,9 +130,9 @@ export async function checkDatabasesHealthError() {
|
|||||||
|
|
||||||
if (diffMinutes > 10) {
|
if (diffMinutes > 10) {
|
||||||
if ((database.healthErrorCount ?? 0) < 3) {
|
if ((database.healthErrorCount ?? 0) < 3) {
|
||||||
|
const newHealthErrorCount = (database.healthErrorCount ?? 0) + 1;
|
||||||
const newHealthErrorCount = (database.healthErrorCount ?? 0) + 1
|
await db
|
||||||
await db.update(drizzleDb.schemas.database)
|
.update(drizzleDb.schemas.database)
|
||||||
.set({
|
.set({
|
||||||
healthErrorCount: newHealthErrorCount,
|
healthErrorCount: newHealthErrorCount,
|
||||||
})
|
})
|
||||||
@@ -141,24 +144,30 @@ export async function checkDatabasesHealthError() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const defaultPolicy = settings?.notificationChannel
|
const defaultPolicy = settings?.notificationChannel
|
||||||
? [{
|
? [
|
||||||
|
{
|
||||||
id: null,
|
id: null,
|
||||||
notificationChannelId: settings.notificationChannel.id,
|
notificationChannelId: settings.notificationChannel.id,
|
||||||
enabled: settings.notificationChannel.enabled,
|
enabled: settings.notificationChannel.enabled,
|
||||||
eventKinds: ["error_health_database"]
|
eventKinds: ["error_health_database"],
|
||||||
}]
|
},
|
||||||
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const policiesToUse = (database.alertPolicies && database.alertPolicies.length > 0)
|
const policiesToUse =
|
||||||
? database.alertPolicies.filter(policy => policy.enabled && policy.eventKinds.includes("error_health_database"))
|
database.alertPolicies && database.alertPolicies.length > 0
|
||||||
|
? database.alertPolicies.filter(
|
||||||
|
(policy) =>
|
||||||
|
policy.enabled &&
|
||||||
|
policy.eventKinds.includes("error_health_database"),
|
||||||
|
)
|
||||||
: defaultPolicy;
|
: defaultPolicy;
|
||||||
|
|
||||||
if (!policiesToUse || policiesToUse.length === 0) {
|
if (!policiesToUse || policiesToUse.length === 0) {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises = policiesToUse.map(alertPolicy => {
|
const promises = policiesToUse.map((alertPolicy) => {
|
||||||
|
|
||||||
const payload: EventPayload = {
|
const payload: EventPayload = {
|
||||||
title: "Database down",
|
title: "Database down",
|
||||||
message: `Database ${database.name} is down, (notification number: ${newHealthErrorCount}/3)`,
|
message: `Database ${database.name} is down, (notification number: ${newHealthErrorCount}/3)`,
|
||||||
@@ -171,9 +180,17 @@ export async function checkDatabasesHealthError() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
log.info({name: "checkDatabasesHealthError", payload: payload},`Database Healthcheck Notification`)
|
log.info(
|
||||||
|
{ name: "checkDatabasesHealthError", payload: payload },
|
||||||
|
`Database Healthcheck Notification`,
|
||||||
|
);
|
||||||
|
|
||||||
return dispatchNotification(payload, alertPolicy.id == null ? undefined : alertPolicy.id, alertPolicy.id ? undefined : alertPolicy.notificationChannelId, undefined);
|
return dispatchNotification(
|
||||||
|
payload,
|
||||||
|
alertPolicy.id == null ? undefined : alertPolicy.id,
|
||||||
|
alertPolicy.id ? undefined : alertPolicy.notificationChannelId,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
@@ -181,5 +198,3 @@ export async function checkDatabasesHealthError() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import {desc, eq} from "drizzle-orm";
|
"use server";
|
||||||
import {db} from "@/db";
|
|
||||||
|
import { desc, eq, isNull } from "drizzle-orm";
|
||||||
|
import { db } from "@/db";
|
||||||
import {
|
import {
|
||||||
NotificationChannel,
|
NotificationChannel,
|
||||||
notificationChannel,
|
notificationChannel,
|
||||||
organizationNotificationChannel
|
organizationNotificationChannel,
|
||||||
} from "@/db/schema/09_notification-channel";
|
} from "@/db/schema/09_notification-channel";
|
||||||
import {storageChannel} from "@/db/schema/12_storage-channel";
|
|
||||||
|
|
||||||
export async function getOrganizationChannels(organizationId: string) {
|
export async function getOrganizationChannels(
|
||||||
return await db
|
organizationId: string,
|
||||||
|
): Promise<NotificationChannel[]> {
|
||||||
|
const [orgChannels, systemChannels] = await Promise.all([
|
||||||
|
db
|
||||||
.select({
|
.select({
|
||||||
id: notificationChannel.id,
|
id: notificationChannel.id,
|
||||||
name: notificationChannel.name,
|
name: notificationChannel.name,
|
||||||
@@ -18,13 +22,41 @@ export async function getOrganizationChannels(organizationId: string) {
|
|||||||
updatedAt: notificationChannel.updatedAt,
|
updatedAt: notificationChannel.updatedAt,
|
||||||
createdAt: notificationChannel.createdAt,
|
createdAt: notificationChannel.createdAt,
|
||||||
deletedAt: notificationChannel.deletedAt,
|
deletedAt: notificationChannel.deletedAt,
|
||||||
organizationId: notificationChannel.organizationId
|
organizationId: notificationChannel.organizationId,
|
||||||
})
|
})
|
||||||
.from(organizationNotificationChannel)
|
.from(organizationNotificationChannel)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
notificationChannel,
|
notificationChannel,
|
||||||
eq(organizationNotificationChannel.notificationChannelId, notificationChannel.id)
|
eq(
|
||||||
|
organizationNotificationChannel.notificationChannelId,
|
||||||
|
notificationChannel.id,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.orderBy(desc(notificationChannel.createdAt))
|
.orderBy(desc(notificationChannel.createdAt))
|
||||||
.where(eq(organizationNotificationChannel.organizationId, organizationId)) as unknown as NotificationChannel[];
|
.where(
|
||||||
|
eq(organizationNotificationChannel.organizationId, organizationId),
|
||||||
|
),
|
||||||
|
db
|
||||||
|
.select({
|
||||||
|
id: notificationChannel.id,
|
||||||
|
name: notificationChannel.name,
|
||||||
|
provider: notificationChannel.provider,
|
||||||
|
config: notificationChannel.config,
|
||||||
|
enabled: notificationChannel.enabled,
|
||||||
|
updatedAt: notificationChannel.updatedAt,
|
||||||
|
createdAt: notificationChannel.createdAt,
|
||||||
|
deletedAt: notificationChannel.deletedAt,
|
||||||
|
organizationId: notificationChannel.organizationId,
|
||||||
|
})
|
||||||
|
.from(notificationChannel)
|
||||||
|
.orderBy(desc(notificationChannel.createdAt))
|
||||||
|
.where(isNull(notificationChannel.organizationId)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return [...orgChannels, ...systemChannels].filter((c) => {
|
||||||
|
if (seen.has(c.id)) return false;
|
||||||
|
seen.add(c.id);
|
||||||
|
return true;
|
||||||
|
}) as NotificationChannel[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import {and, desc, eq, gte, lte} from 'drizzle-orm';
|
"use server";
|
||||||
import {NotificationLevel, notificationLog} from "@/db/schema/11_notification-log";
|
|
||||||
import {notificationChannel} from "@/db/schema/09_notification-channel";
|
import { and, desc, eq, gte, lte } from "drizzle-orm";
|
||||||
import {db} from "@/db";
|
import {
|
||||||
import {Json} from "drizzle-zod";
|
NotificationLevel,
|
||||||
|
notificationLog,
|
||||||
|
} from "@/db/schema/11_notification-log";
|
||||||
|
import { notificationChannel } from "@/db/schema/09_notification-channel";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { Json } from "drizzle-zod";
|
||||||
|
|
||||||
export type NotificationLogWithRelations = {
|
export type NotificationLogWithRelations = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -15,7 +20,7 @@ export type NotificationLogWithRelations = {
|
|||||||
content: {
|
content: {
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
},
|
};
|
||||||
channel: {
|
channel: {
|
||||||
name: string;
|
name: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
@@ -25,8 +30,7 @@ export type NotificationLogWithRelations = {
|
|||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getNotificationHistory(
|
export async function getNotificationHistory(filters?: {
|
||||||
filters?: {
|
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
policyId?: string;
|
policyId?: string;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
@@ -35,14 +39,17 @@ export async function getNotificationHistory(
|
|||||||
from?: Date;
|
from?: Date;
|
||||||
to?: Date;
|
to?: Date;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}): Promise<NotificationLogWithRelations[]> {
|
||||||
): Promise<NotificationLogWithRelations[]> {
|
|
||||||
const where = [];
|
const where = [];
|
||||||
if (filters?.channelId) where.push(eq(notificationLog.channelId, filters.channelId));
|
if (filters?.channelId)
|
||||||
if (filters?.policyId) where.push(eq(notificationLog.policyId, filters.policyId));
|
where.push(eq(notificationLog.channelId, filters.channelId));
|
||||||
if (filters?.organizationId) where.push(eq(notificationLog.organizationId, filters.organizationId));
|
if (filters?.policyId)
|
||||||
|
where.push(eq(notificationLog.policyId, filters.policyId));
|
||||||
|
if (filters?.organizationId)
|
||||||
|
where.push(eq(notificationLog.organizationId, filters.organizationId));
|
||||||
if (filters?.level) where.push(eq(notificationLog.level, filters.level));
|
if (filters?.level) where.push(eq(notificationLog.level, filters.level));
|
||||||
if (typeof filters?.success === 'boolean') where.push(eq(notificationLog.success, filters.success));
|
if (typeof filters?.success === "boolean")
|
||||||
|
where.push(eq(notificationLog.success, filters.success));
|
||||||
if (filters?.from) where.push(gte(notificationLog.sentAt, filters.from));
|
if (filters?.from) where.push(gte(notificationLog.sentAt, filters.from));
|
||||||
if (filters?.to) where.push(lte(notificationLog.sentAt, filters.to));
|
if (filters?.to) where.push(lte(notificationLog.sentAt, filters.to));
|
||||||
|
|
||||||
@@ -68,13 +75,16 @@ export async function getNotificationHistory(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.from(notificationLog)
|
.from(notificationLog)
|
||||||
.leftJoin(notificationChannel, eq(notificationLog.channelId, notificationChannel.id))
|
.leftJoin(
|
||||||
|
notificationChannel,
|
||||||
|
eq(notificationLog.channelId, notificationChannel.id),
|
||||||
|
)
|
||||||
// .leftJoin(alertPolicy, eq(notificationLog.policyId, alertPolicy.id))
|
// .leftJoin(alertPolicy, eq(notificationLog.policyId, alertPolicy.id))
|
||||||
.where(and(...where))
|
.where(and(...where))
|
||||||
.orderBy(desc(notificationLog.sentAt))
|
.orderBy(desc(notificationLog.sentAt))
|
||||||
.limit(filters?.limit || 100);
|
.limit(filters?.limit || 100);
|
||||||
|
|
||||||
return rows.map(row => ({
|
return rows.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
payload: row.payload as Json,
|
payload: row.payload as Json,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { and, eq, inArray, ne } from "drizzle-orm";
|
||||||
|
import { member } from "@/db/schema/04_member";
|
||||||
|
import { organization } from "@/db/schema/03_organization";
|
||||||
|
|
||||||
|
export async function getUserOrganization(userId: string) {
|
||||||
|
const memberRow = await db.query.member.findFirst({
|
||||||
|
columns: { organizationId: true },
|
||||||
|
where: eq(member.userId, userId),
|
||||||
|
});
|
||||||
|
if (!memberRow) return null;
|
||||||
|
return db.query.organization.findFirst({
|
||||||
|
where: eq(organization.id, memberRow.organizationId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserOwnOrganization(userId: string) {
|
||||||
|
const memberRows = await db.query.member.findMany({
|
||||||
|
columns: { organizationId: true },
|
||||||
|
where: eq(member.userId, userId),
|
||||||
|
});
|
||||||
|
if (!memberRows.length) return null;
|
||||||
|
const orgIds = memberRows.map((r) => r.organizationId);
|
||||||
|
return db.query.organization.findFirst({
|
||||||
|
where: and(inArray(organization.id, orgIds), ne(organization.slug, "default")),
|
||||||
|
});
|
||||||
|
}
|
||||||
+30
-14
@@ -1,14 +1,28 @@
|
|||||||
import {getOrganization} from "@/lib/auth/auth";
|
"use server";
|
||||||
import {db} from "@/db";
|
|
||||||
import {and, eq} from "drizzle-orm";
|
import { getOrganization } from "@/lib/auth/auth";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
import { project } from "@/db/schema/06_project";
|
||||||
|
|
||||||
|
export async function getOrganizationProject(organizationId: string) {
|
||||||
|
return db.query.project.findFirst({
|
||||||
|
where: eq(project.organizationId, organizationId),
|
||||||
|
with: {
|
||||||
|
databases: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const getOrganizationProjectDatabases = async ({organizationSlug, projectId}: {
|
export const getOrganizationProjectDatabases = async ({
|
||||||
organizationSlug: string, projectId: string
|
organizationSlug,
|
||||||
|
projectId,
|
||||||
|
}: {
|
||||||
|
organizationSlug: string;
|
||||||
|
projectId: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
@@ -20,10 +34,13 @@ export const getOrganizationProjectDatabases = async ({organizationSlug, project
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const databasesProject = await db.query.project.findFirst({
|
const databasesProject = await db.query.project.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.project.organizationId, organization.id), eq(drizzleDb.schemas.project.id, projectId)),
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.project.organizationId, organization.id),
|
||||||
|
eq(drizzleDb.schemas.project.id, projectId),
|
||||||
|
),
|
||||||
with: {
|
with: {
|
||||||
databases: true
|
databases: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!databasesProject) {
|
if (!databasesProject) {
|
||||||
@@ -37,12 +54,11 @@ export const getOrganizationProjectDatabases = async ({organizationSlug, project
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
data: databasesProject.databases,
|
data: databasesProject.databases,
|
||||||
ids: databasesProject.databases.map((project) => project.id)
|
ids: databasesProject.databases.map((project) => project.id),
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const errorMessage = e?.response?.data?.message || e?.message || "Unknown auth error";
|
const errorMessage =
|
||||||
|
e?.response?.data?.message || e?.message || "Unknown auth error";
|
||||||
const status = e?.response?.status || 500;
|
const status = e?.response?.status || 500;
|
||||||
|
|
||||||
console.error("API GettingOrganizationProjectDatabases error:", {
|
console.error("API GettingOrganizationProjectDatabases error:", {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { db } from "@/db";
|
||||||
|
|
||||||
|
export async function getSettings() {
|
||||||
|
return db.query.setting.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isOnboardingDone(): Promise<boolean> {
|
||||||
|
const settings = await db.query.setting.findFirst();
|
||||||
|
return settings?.onboarding ?? false;
|
||||||
|
}
|
||||||
@@ -1,9 +1,18 @@
|
|||||||
import {desc, eq} from "drizzle-orm";
|
"use server";
|
||||||
import {db} from "@/db";
|
|
||||||
import {organizationStorageChannel, StorageChannel, storageChannel} from "@/db/schema/12_storage-channel";
|
|
||||||
|
|
||||||
export async function getOrganizationStorageChannels(organizationId: string) {
|
import { desc, eq, isNull } from "drizzle-orm";
|
||||||
return await db
|
import { db } from "@/db";
|
||||||
|
import {
|
||||||
|
organizationStorageChannel,
|
||||||
|
StorageChannel,
|
||||||
|
storageChannel,
|
||||||
|
} from "@/db/schema/12_storage-channel";
|
||||||
|
|
||||||
|
export async function getOrganizationStorageChannels(
|
||||||
|
organizationId: string,
|
||||||
|
): Promise<StorageChannel[]> {
|
||||||
|
const [orgChannels, systemChannels] = await Promise.all([
|
||||||
|
db
|
||||||
.select({
|
.select({
|
||||||
id: storageChannel.id,
|
id: storageChannel.id,
|
||||||
name: storageChannel.name,
|
name: storageChannel.name,
|
||||||
@@ -18,8 +27,31 @@ export async function getOrganizationStorageChannels(organizationId: string) {
|
|||||||
.from(organizationStorageChannel)
|
.from(organizationStorageChannel)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
storageChannel,
|
storageChannel,
|
||||||
eq(organizationStorageChannel.storageChannelId, storageChannel.id)
|
eq(organizationStorageChannel.storageChannelId, storageChannel.id),
|
||||||
)
|
)
|
||||||
.orderBy(desc(storageChannel.createdAt))
|
.orderBy(desc(storageChannel.createdAt))
|
||||||
.where(eq(organizationStorageChannel.organizationId, organizationId)) as unknown as StorageChannel[];
|
.where(eq(organizationStorageChannel.organizationId, organizationId)),
|
||||||
|
db
|
||||||
|
.select({
|
||||||
|
id: storageChannel.id,
|
||||||
|
name: storageChannel.name,
|
||||||
|
provider: storageChannel.provider,
|
||||||
|
organizationId: storageChannel.organizationId,
|
||||||
|
config: storageChannel.config,
|
||||||
|
enabled: storageChannel.enabled,
|
||||||
|
updatedAt: storageChannel.updatedAt,
|
||||||
|
createdAt: storageChannel.createdAt,
|
||||||
|
deletedAt: storageChannel.deletedAt,
|
||||||
|
})
|
||||||
|
.from(storageChannel)
|
||||||
|
.orderBy(desc(storageChannel.createdAt))
|
||||||
|
.where(isNull(storageChannel.organizationId)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return [...orgChannels, ...systemChannels].filter((c) => {
|
||||||
|
if (seen.has(c.id)) return false;
|
||||||
|
seen.add(c.id);
|
||||||
|
return true;
|
||||||
|
}) as StorageChannel[];
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-9
@@ -1,10 +1,16 @@
|
|||||||
import {SignUpUser} from "@/types/auth";
|
"use server";
|
||||||
import {hashPassword} from "better-auth/crypto";
|
|
||||||
import {db} from "@/db";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
import {User, UserThemeEnum} from "@/db/schema/02_user";
|
|
||||||
import {assertValidPassword} from "@/utils/password";
|
|
||||||
|
|
||||||
|
import { SignUpUser } from "@/types/auth";
|
||||||
|
import { hashPassword } from "better-auth/crypto";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import { User, UserThemeEnum } from "@/db/schema/02_user";
|
||||||
|
import { assertValidPassword } from "@/utils/password";
|
||||||
|
|
||||||
|
export async function hasUsers(): Promise<boolean> {
|
||||||
|
const result = await db.select().from(drizzleDb.schemas.user).limit(1);
|
||||||
|
return result.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
export async function createUserDb(data: SignUpUser): Promise<User> {
|
export async function createUserDb(data: SignUpUser): Promise<User> {
|
||||||
assertValidPassword(data.password);
|
assertValidPassword(data.password);
|
||||||
@@ -12,7 +18,9 @@ export async function createUserDb(data: SignUpUser): Promise<User> {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
const userId = crypto.randomUUID();
|
const userId = crypto.randomUUID();
|
||||||
|
|
||||||
const [newUser] = await db.insert(drizzleDb.schemas.user).values({
|
const [newUser] = await db
|
||||||
|
.insert(drizzleDb.schemas.user)
|
||||||
|
.values({
|
||||||
...data,
|
...data,
|
||||||
id: userId,
|
id: userId,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -22,7 +30,8 @@ export async function createUserDb(data: SignUpUser): Promise<User> {
|
|||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
theme: data.theme as UserThemeEnum,
|
theme: data.theme as UserThemeEnum,
|
||||||
}).returning();
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
if (data.password) {
|
if (data.password) {
|
||||||
const hashedPassword = await hashPassword(data.password);
|
const hashedPassword = await hashPassword(data.password);
|
||||||
@@ -36,5 +45,5 @@ export async function createUserDb(data: SignUpUser): Promise<User> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return newUser
|
return newUser;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ export const env = createEnv({
|
|||||||
|
|
||||||
ALLOWED_GROUP: z.string().optional(),
|
ALLOWED_GROUP: z.string().optional(),
|
||||||
|
|
||||||
|
SKIP_ONBOARDING: z.string().optional().default("false"),
|
||||||
|
|
||||||
AUTH_EMAIL_PASSWORD_ENABLED: z.string().optional().default("true"),
|
AUTH_EMAIL_PASSWORD_ENABLED: z.string().optional().default("true"),
|
||||||
AUTH_SIGNUP_ENABLED: z.string().optional().default("true"),
|
AUTH_SIGNUP_ENABLED: z.string().optional().default("true"),
|
||||||
AUTH_PASSKEY_ENABLED: z.string().optional().default("false"),
|
AUTH_PASSKEY_ENABLED: z.string().optional().default("false"),
|
||||||
@@ -174,6 +176,8 @@ export const env = createEnv({
|
|||||||
|
|
||||||
ALLOWED_GROUP: process.env.ALLOWED_GROUP,
|
ALLOWED_GROUP: process.env.ALLOWED_GROUP,
|
||||||
|
|
||||||
|
SKIP_ONBOARDING: process.env.SKIP_ONBOARDING,
|
||||||
|
|
||||||
AUTH_EMAIL_PASSWORD_ENABLED: process.env.AUTH_EMAIL_PASSWORD_ENABLED,
|
AUTH_EMAIL_PASSWORD_ENABLED: process.env.AUTH_EMAIL_PASSWORD_ENABLED,
|
||||||
AUTH_SIGNUP_ENABLED: process.env.AUTH_SIGNUP_ENABLED,
|
AUTH_SIGNUP_ENABLED: process.env.AUTH_SIGNUP_ENABLED,
|
||||||
AUTH_PASSKEY_ENABLED: process.env.AUTH_PASSKEY_ENABLED,
|
AUTH_PASSKEY_ENABLED: process.env.AUTH_PASSKEY_ENABLED,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import {action, ActionError, userAction} from "@/lib/safe-actions/actions";
|
import {action, ActionError, userAction} from "@/lib/safe-actions/actions";
|
||||||
import {AgentSchema} from "@/features/agents/agents.schema";
|
import {AgentSchema} from "@/features/agents/schemas/agents.schema";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {eq, and, ne, count, desc} from "drizzle-orm";
|
import {eq, and, ne, count, desc} from "drizzle-orm";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
@@ -11,7 +11,7 @@ import {Badge} from "@/components/ui/badge";
|
|||||||
import {truncateWords} from "@/utils/text";
|
import {truncateWords} from "@/utils/text";
|
||||||
import {useIsMobile} from "@/hooks/use-mobile";
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
||||||
import {useAgentUpdateCheck} from "@/features/agents/use-agent-update-check";
|
import {useAgentUpdateCheck} from "@/features/agents/hooks/use-agent-update-check";
|
||||||
|
|
||||||
export type agentCardProps = {
|
export type agentCardProps = {
|
||||||
data: AgentWith;
|
data: AgentWith;
|
||||||
+4
-4
@@ -3,10 +3,10 @@
|
|||||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import {Server} from "lucide-react";
|
import {Server} from "lucide-react";
|
||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||||
import {AgentCardKey} from "@/features/agents/agent-card-key";
|
import {AgentCardKey} from "@/features/agents/components/agent-card-key";
|
||||||
import {AgentWithDatabases} from "@/db/schema/08_agent";
|
import {AgentWithDatabases} from "@/db/schema/08_agent";
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import {useQuery} from "@tanstack/react-query";
|
||||||
import {getAgentAction} from "@/features/agents/agents.action";
|
import {getAgentAction} from "@/features/agents/actions/agents.action";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
import {Separator} from "@/components/ui/separator";
|
import {Separator} from "@/components/ui/separator";
|
||||||
import {Badge} from "@/components/ui/badge";
|
import {Badge} from "@/components/ui/badge";
|
||||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
||||||
import {AgentDatabaseCard} from "@/features/agents/agent-database-card";
|
import {AgentDatabaseCard} from "@/features/agents/components/agent-database-card";
|
||||||
import {HealthCheckGraph} from "@/features/database/health-grid";
|
import {HealthCheckGraph} from "@/features/database/components/health-grid";
|
||||||
import {HealthcheckLog} from "@/db/schema/15_healthcheck-log";
|
import {HealthcheckLog} from "@/db/schema/15_healthcheck-log";
|
||||||
|
|
||||||
type AgentContentPageProps = {
|
type AgentContentPageProps = {
|
||||||
+1
-1
@@ -5,7 +5,7 @@ import {ButtonWithConfirm} from "@/components/common/button-with-confirm";
|
|||||||
import {useMutation} from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {deleteAgentAction} from "@/features/agents/agent-delete.action";
|
import {deleteAgentAction} from "@/features/agents/actions/agent-delete.action";
|
||||||
import {useIsMobile} from "@/hooks/use-mobile";
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
|
|
||||||
export type ButtonDeleteAgentProps = {
|
export type ButtonDeleteAgentProps = {
|
||||||
+2
-2
@@ -7,7 +7,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {AgentForm} from "@/features/agents/agent-form";
|
import {AgentForm} from "@/features/agents/components/agent-form";
|
||||||
import {Button, buttonVariants} from "@/components/ui/button";
|
import {Button, buttonVariants} from "@/components/ui/button";
|
||||||
import {Plus} from "lucide-react";
|
import {Plus} from "lucide-react";
|
||||||
import {GearIcon} from "@radix-ui/react-icons";
|
import {GearIcon} from "@radix-ui/react-icons";
|
||||||
@@ -16,7 +16,7 @@ import {useState} from "react";
|
|||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||||
import {AgentOrganisationForm} from "@/features/agents/agent-organizations.form";
|
import {AgentOrganisationForm} from "@/features/agents/components/agent-organizations.form";
|
||||||
import {AgentWith} from "@/db/schema/08_agent";
|
import {AgentWith} from "@/db/schema/08_agent";
|
||||||
|
|
||||||
type AgentDialogProps = {
|
type AgentDialogProps = {
|
||||||
@@ -15,9 +15,9 @@ import {Button} from "@/components/ui/button";
|
|||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||||
import {AgentSchema, AgentType} from "@/features/agents/agents.schema";
|
import {AgentSchema, AgentType} from "@/features/agents/schemas/agents.schema";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {createAgentAction, updateAgentAction} from "@/features/agents/agents.action";
|
import {createAgentAction, updateAgentAction} from "@/features/agents/actions/agents.action";
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
|
|
||||||
export type agentFormProps = {
|
export type agentFormProps = {
|
||||||
+2
-2
@@ -8,8 +8,8 @@ import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
|||||||
import {MultiSelect} from "@/components/common/multi-select";
|
import {MultiSelect} from "@/components/common/multi-select";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {AgentWith} from "@/db/schema/08_agent";
|
import {AgentWith} from "@/db/schema/08_agent";
|
||||||
import {AgentOrganizationSchema, AgentOrganizationType} from "@/features/agents/agent-organizations.schema";
|
import {AgentOrganizationSchema, AgentOrganizationType} from "@/features/agents/schemas/agent-organizations.schema";
|
||||||
import {updateAgentOrganizationsAction} from "@/features/agents/agent-organizations.action";
|
import {updateAgentOrganizationsAction} from "@/features/agents/actions/agent-organizations.action";
|
||||||
|
|
||||||
|
|
||||||
type AgentOrganisationFormProps = {
|
type AgentOrganisationFormProps = {
|
||||||
+1
-1
@@ -9,7 +9,7 @@ import { Form } from "@/components/ui/form";
|
|||||||
import { requestPasswordReset } from "@/lib/auth/auth-client";
|
import { requestPasswordReset } from "@/lib/auth/auth-client";
|
||||||
import { ButtonWithLoading } from "@/components/common/button-with-loading";
|
import { ButtonWithLoading } from "@/components/common/button-with-loading";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ForgotPasswordSchema, ForgotPasswordType } from "./forgot-password-form.schema";
|
import { ForgotPasswordSchema, ForgotPasswordType } from "../schemas/forgot-password-form.schema";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
|
||||||
export type ForgotPasswordFormProps = {
|
export type ForgotPasswordFormProps = {
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import TwoFactorForm from "@/features/profile/2fa-form";
|
import TwoFactorForm from "@/features/profile/components/2fa-form";
|
||||||
|
|
||||||
export const GuardForm = () => {
|
export const GuardForm = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -19,7 +19,7 @@ import { PasswordInput } from "@/components/ui/password-input";
|
|||||||
import {
|
import {
|
||||||
LoginSchema,
|
LoginSchema,
|
||||||
type LoginType,
|
type LoginType,
|
||||||
} from "@/features/auth/login-form.schema";
|
} from "@/features/auth/schemas/login-form.schema";
|
||||||
import { ButtonWithLoading } from "@/components/common/button-with-loading";
|
import { ButtonWithLoading } from "@/components/common/button-with-loading";
|
||||||
import { signIn } from "@/lib/auth/auth-client";
|
import { signIn } from "@/lib/auth/auth-client";
|
||||||
|
|
||||||
+2
-2
@@ -14,10 +14,10 @@ import {
|
|||||||
import {
|
import {
|
||||||
RegisterSchema,
|
RegisterSchema,
|
||||||
RegisterType,
|
RegisterType,
|
||||||
} from "@/features/auth/register-form.schema";
|
} from "@/features/auth/schemas/register-form.schema";
|
||||||
import { signUp } from "@/lib/auth/auth-client";
|
import { signUp } from "@/lib/auth/auth-client";
|
||||||
import { useZodForm } from "@/components/ui/form";
|
import { useZodForm } from "@/components/ui/form";
|
||||||
import { CardAuth } from "@/features/layout/card-auth";
|
import { CardAuth } from "@/features/layout/components/card-auth";
|
||||||
import { FormField } from "@/components/ui/form";
|
import { FormField } from "@/components/ui/form";
|
||||||
import { FormItem } from "@/components/ui/form";
|
import { FormItem } from "@/components/ui/form";
|
||||||
import { FormLabel } from "@/components/ui/form";
|
import { FormLabel } from "@/components/ui/form";
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import {FormControl, FormField, FormItem, FormLabel, useZodForm} from "@/compone
|
|||||||
import {Form} from "@/components/ui/form";
|
import {Form} from "@/components/ui/form";
|
||||||
import {ButtonWithLoading} from "@/components/common/button-with-loading";
|
import {ButtonWithLoading} from "@/components/common/button-with-loading";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {ResetPasswordSchema, ResetPasswordType} from "./reset-password-form.schema";
|
import {ResetPasswordSchema, ResetPasswordType} from "../schemas/reset-password-form.schema";
|
||||||
import {PasswordStrengthInput} from "@/components/ui/password-input-indicator";
|
import {PasswordStrengthInput} from "@/components/ui/password-input-indicator";
|
||||||
import {useRouter, useSearchParams} from "next/navigation";
|
import {useRouter, useSearchParams} from "next/navigation";
|
||||||
import {ArrowLeft} from "lucide-react";
|
import {ArrowLeft} from "lucide-react";
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { authClient, passkey } from "@/lib/auth/auth-client";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import { AuthProviderConfig } from "@/lib/auth/config";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
export function SocialAuthButtons({
|
||||||
|
providers,
|
||||||
|
}: {
|
||||||
|
providers: AuthProviderConfig[];
|
||||||
|
}) {
|
||||||
|
const socialProviders = providers.filter(
|
||||||
|
(p) => p.isActive && p.type !== "credential",
|
||||||
|
);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleSocialSignIn = async (provider: AuthProviderConfig) => {
|
||||||
|
setIsLoading(provider.id);
|
||||||
|
try {
|
||||||
|
let result;
|
||||||
|
if (provider.id === "passkey") {
|
||||||
|
result = await authClient.signIn.passkey({
|
||||||
|
fetchOptions: {
|
||||||
|
onSuccess() {
|
||||||
|
router.push("/dashboard");
|
||||||
|
},
|
||||||
|
onError() {
|
||||||
|
toast.error(
|
||||||
|
"An error occurred during passkey authentication. Please try again.",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (provider.type === "sso") {
|
||||||
|
result = await authClient.signIn.sso({
|
||||||
|
providerId: provider.id,
|
||||||
|
providerType: "oidc",
|
||||||
|
callbackURL: "/dashboard",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result = await authClient.signIn.social({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
provider: provider.id as any,
|
||||||
|
callbackURL: "/dashboard",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
toast.error(
|
||||||
|
"An error occurred while signing in with the provider. Please try again.",
|
||||||
|
);
|
||||||
|
} else if (provider.id !== "passkey") {
|
||||||
|
toast.success("Redirecting to provider...");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(
|
||||||
|
"An error occurred while signing in with the provider. Please try again.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (socialProviders.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
{socialProviders.map((provider) => (
|
||||||
|
<Button
|
||||||
|
key={provider.id}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full gap-2"
|
||||||
|
onClick={() => handleSocialSignIn(provider)}
|
||||||
|
disabled={!!isLoading}
|
||||||
|
>
|
||||||
|
{isLoading === provider.id ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : provider.icon.startsWith("/") ||
|
||||||
|
provider.icon.startsWith("http") ? (
|
||||||
|
<Image
|
||||||
|
src={provider.icon}
|
||||||
|
alt={provider.id || "icon"}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className="h-4 w-4"
|
||||||
|
unoptimized={provider.icon.startsWith("http")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon icon={provider.icon} className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
<span>{provider.title || provider.name}</span>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user