From 56603e7e997456a330c80d1d7d4522f46e1b15b8 Mon Sep 17 00:00:00 2001 From: Aidan Date: Sun, 16 Feb 2025 15:28:17 -0500 Subject: [PATCH] i don't even know at this point (3 billion changes to build the first release) --- .gitea/workflows/ci.yaml | 28 ++ .gitignore | 10 +- Dockerfile | 1 + README.md | 50 ++- actions/logout.ts | 7 + app/account/dashboard/ai/page.tsx | 37 ++ app/account/dashboard/downloads/page.tsx | 51 +++ app/account/dashboard/exchange/page.tsx | 28 ++ app/account/dashboard/layout.tsx | 37 ++ app/account/dashboard/page.tsx | 260 ++++--------- app/account/dashboard/settings/page.tsx | 26 ++ app/account/dashboard/statistics/page.tsx | 64 +++ app/account/dashboard/upgrades/page.tsx | 27 ++ app/account/layout.tsx | 7 +- app/account/login/code/page.tsx | 118 ------ app/account/login/page.tsx | 92 ++--- app/account/logout/page.tsx | 77 ---- app/account/page.tsx | 14 + app/account/settings/page.tsx | 38 -- app/account/signup/page.tsx | 365 ++++++++++++++++++ app/account/signup/success/page.tsx | 160 ++++++++ app/api/auth/[...nextauth]/route.ts | 2 + app/api/git/user/route.ts | 46 +++ app/api/users/create/route.ts | 125 ++++++ app/components/FeatureCard.tsx | 31 -- app/components/Navbar.tsx | 87 ----- app/components/account/Sidebar.tsx | 38 -- app/globals.css | 105 +++-- app/layout.tsx | 33 +- app/page.tsx | 55 ++- app/providers.tsx | 13 + auth.ts | 14 + bun.lockb | Bin 207748 -> 223530 bytes components/cards/dashboard/DiskUsage.tsx | 32 ++ .../dashboard/overview/LinkedAccounts.tsx | 85 ++++ .../cards/dashboard/overview/WelcomeCard.tsx | 45 +++ components/custom/LogoutMenuItem.tsx | 20 + components/custom/SidebarToggle.tsx | 25 ++ components/custom/ThemeToggle.tsx | 43 +++ components/pages/dashboard/Footer.tsx | 28 ++ components/pages/dashboard/GitTab.tsx | 197 ++++++++++ components/pages/dashboard/Header.tsx | 104 +++++ components/pages/dashboard/OverviewTab.tsx | 12 + components/pages/dashboard/SecurityTab.tsx | 38 ++ components/pages/dashboard/ServicesTab.tsx | 83 ++++ components/pages/dashboard/SideMenu.tsx | 116 ++++++ .../pages/dashboard/downloads/EmailTab.tsx | 185 +++++++++ .../pages/dashboard/downloads/GitTab.tsx | 64 +++ .../pages/dashboard/downloads/HomeTab.tsx | 62 +++ .../pages/dashboard/downloads/PassTab.tsx | 170 ++++++++ components/pages/main/FeatureCard.tsx | 26 ++ .../pages/main}/Hero.tsx | 26 +- components/pages/main/Navbar.tsx | 99 +++++ components/theme-provider.tsx | 11 + components/ui/avatar.tsx | 50 +++ components/ui/badge.tsx | 36 ++ components/ui/collapsible.tsx | 11 + components/ui/command.tsx | 153 ++++++++ components/ui/dialog.tsx | 122 ++++++ components/ui/dropdown-menu.tsx | 201 ++++++++++ components/ui/form.tsx | 178 +++++++++ components/ui/label.tsx | 26 ++ components/ui/popover.tsx | 33 ++ components/ui/radio-group.tsx | 44 +++ components/ui/select.tsx | 159 ++++++++ components/ui/switch.tsx | 29 ++ components/ui/table.tsx | 120 ++++++ components/ui/tabs.tsx | 55 +++ components/ui/textarea.tsx | 22 ++ hooks/use-mobile.tsx | 2 +- lib/prisma.ts | 8 + middleware.ts | 5 + package.json | 49 ++- prisma/schema.prisma | 17 + public/noise-dark.png | Bin 0 -> 11008055 bytes public/noise-light.png | Bin 0 -> 7272977 bytes tailwind.config.ts | 153 +++++--- 77 files changed, 4151 insertions(+), 839 deletions(-) create mode 100644 .gitea/workflows/ci.yaml create mode 100644 actions/logout.ts create mode 100644 app/account/dashboard/ai/page.tsx create mode 100644 app/account/dashboard/downloads/page.tsx create mode 100644 app/account/dashboard/exchange/page.tsx create mode 100644 app/account/dashboard/layout.tsx create mode 100644 app/account/dashboard/settings/page.tsx create mode 100644 app/account/dashboard/statistics/page.tsx create mode 100644 app/account/dashboard/upgrades/page.tsx delete mode 100644 app/account/login/code/page.tsx delete mode 100644 app/account/logout/page.tsx create mode 100644 app/account/page.tsx delete mode 100644 app/account/settings/page.tsx create mode 100644 app/account/signup/page.tsx create mode 100644 app/account/signup/success/page.tsx create mode 100644 app/api/auth/[...nextauth]/route.ts create mode 100644 app/api/git/user/route.ts create mode 100644 app/api/users/create/route.ts delete mode 100644 app/components/FeatureCard.tsx delete mode 100644 app/components/Navbar.tsx delete mode 100644 app/components/account/Sidebar.tsx create mode 100644 app/providers.tsx create mode 100644 auth.ts create mode 100644 components/cards/dashboard/DiskUsage.tsx create mode 100644 components/cards/dashboard/overview/LinkedAccounts.tsx create mode 100644 components/cards/dashboard/overview/WelcomeCard.tsx create mode 100644 components/custom/LogoutMenuItem.tsx create mode 100644 components/custom/SidebarToggle.tsx create mode 100644 components/custom/ThemeToggle.tsx create mode 100644 components/pages/dashboard/Footer.tsx create mode 100644 components/pages/dashboard/GitTab.tsx create mode 100644 components/pages/dashboard/Header.tsx create mode 100644 components/pages/dashboard/OverviewTab.tsx create mode 100644 components/pages/dashboard/SecurityTab.tsx create mode 100644 components/pages/dashboard/ServicesTab.tsx create mode 100644 components/pages/dashboard/SideMenu.tsx create mode 100644 components/pages/dashboard/downloads/EmailTab.tsx create mode 100644 components/pages/dashboard/downloads/GitTab.tsx create mode 100644 components/pages/dashboard/downloads/HomeTab.tsx create mode 100644 components/pages/dashboard/downloads/PassTab.tsx create mode 100644 components/pages/main/FeatureCard.tsx rename {app/components => components/pages/main}/Hero.tsx (51%) create mode 100644 components/pages/main/Navbar.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/collapsible.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 lib/prisma.ts create mode 100644 middleware.ts create mode 100644 prisma/schema.prisma create mode 100644 public/noise-dark.png create mode 100644 public/noise-light.png diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..2ebb213 --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,28 @@ +name: Build and Push Nightly CI Image + +on: + schedule: + - cron: '0 0 * * *' + push: + branches: + - main + +jobs: + build_and_push: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Log in to Gitea Package Registry + run: echo "${{ secrets.PACKAGE_TOKEN }}" | docker login $SERVER_URL -u $USERNAME --password-stdin + env: + SERVER_URL: ${{ secrets.SERVER_URL }} + USERNAME: ${{ secrets.USERNAME }} + PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} + + - name: Build Docker Image (ci tag) + run: docker build -t git.pontusmail.org/librecloud/web:ci . + + - name: Push Docker Image (ci tag) + run: docker push git.pontusmail.org/librecloud/web:ci diff --git a/.gitignore b/.gitignore index a9fca84..e7d5bda 100644 --- a/.gitignore +++ b/.gitignore @@ -43,5 +43,13 @@ next-env.d.ts # vs code .vscode/ +# idea +.idea/ + # package-lock -package-lock.json \ No newline at end of file +package-lock.json +bun.lockb + +# prisma +prisma/dev.db +prisma/migrations/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 6dbacfc..21069fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ COPY package.json ./ COPY bun.lockb ./ RUN bun install --frozen-lockfile COPY . . +RUN bunx prisma migrate dev --name init RUN bun run build EXPOSE 3000 CMD ["bun", "start"] diff --git a/README.md b/README.md index fe1ae4b..173f3c1 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,25 @@ -# web +#
web
-LibreCloud's website +![Last Update](https://img.shields.io/badge/last_update-16_Feb_2024-blue) +[![License: CC0-1.0](https://img.shields.io/badge/License-CC0_1.0-lightgrey.svg)](http://creativecommons.org/publicdomain/zero/1.0/) +[![Build Status](https://git.pontusmail.org/librecloud/web/actions/workflows/docker.yaml/badge.svg)](https://git.pontusmail.org/librecloud/web/actions/?workflow=docker.yaml) +[![Build Status](https://git.pontusmail.org/librecloud/web/actions/workflows/ci.yaml/badge.svg)](https://git.pontusmail.org/librecloud/web/actions/?workflow=ci.yaml) + +LibreCloud's website, dashboard, and API ## Docker Instructions -1. **Fetch needed file(s)** +A Docker setup requires both Docker *and* Docker Compose. + +1. **Install Bun if you haven't already** + + Bun is a fast JavaScript runtime, which we prefer over `npm`. These instructions will be written for Bun, but could be adapted to `npm` or `yarn` if needed. + + ```bash + curl -fsSL https://bun.sh/install | bash + ``` + +2. **Fetch needed file(s)** Pick your preferred option to get the file(s) needed for Docker. Either option is fine, although Git is arguably the best option. @@ -22,7 +37,7 @@ LibreCloud's website You may have to install `wget`, or you could use `curl` instead. -2. **Bring up the container** +3. **Bring the container up** ```bash docker compose up -d @@ -32,11 +47,22 @@ LibreCloud's website You may customize the container with the included `docker-compose.yml` file if needed. Your server will start on port `3019` by default. We suggest using a reverse proxy to serve the site on a domain. +4. **Complete Setup** + + If you would like to host the entire LibreCloud frontend and backend, you will also need to setup the following repositories and edit this project to work with *your* setup. + + * [mail-connect](https://git.pontusmail.org/librecloud/mail-connect) + * [docker-mailserver](https://github.com/docker-mailserver/docker-mailserver) + ## Dev Server Instructions 1. **Install Bun if you haven't already** - Bun is a fast JavaScript runtime, and is much faster than `npm`. These instructions will be written for Bun, but could be adapted to `npm` or `yarn` if needed. + Bun is a fast JavaScript runtime, which we prefer over `npm`. These instructions will be written for Bun, but could be adapted to `npm` or `yarn` if needed. + + ```bash + curl -fsSL https://bun.sh/install | bash + ``` 2. **Clone the repo** @@ -51,7 +77,19 @@ LibreCloud's website bun install ``` -4. **Start dev server** +4. **Initialize Prisma** + + Because `web` uses a database for storing Git link statuses (and other things to come), you will need to initialize the SQLite database. + + A `schema.prisma` file has been provided to make this easy. + + This can be done by executing: + + ```bash + bunx prisma migrate dev --name init + ``` + +5. **Start dev server** ```bash bun dev diff --git a/actions/logout.ts b/actions/logout.ts new file mode 100644 index 0000000..ed5e5f1 --- /dev/null +++ b/actions/logout.ts @@ -0,0 +1,7 @@ +"use server"; + +import { signOut } from "@/auth"; + +export async function logout() { + await signOut(); +} \ No newline at end of file diff --git a/app/account/dashboard/ai/page.tsx b/app/account/dashboard/ai/page.tsx new file mode 100644 index 0000000..a38f5e5 --- /dev/null +++ b/app/account/dashboard/ai/page.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { motion } from "framer-motion" +import { SideMenu } from "@/components/pages/dashboard/SideMenu" +import { Sparkles } from "lucide-react"; + +const fadeIn = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + transition: { duration: 0.4 }, +} + +export default function GenAI() { + return ( +
+ +
+
+ + +

Generative AI is coming soon

+

Experience artificial intelligence without the bloat and cost.

+
    +
  • Open-source (and public domain) chat interface
  • +
  • Use the same models you're familiar with
  • +
  • Pay per 1M tokens and save money
  • +
  • Free models for testing/use
  • +
  • ZERO additional fees
  • +
+

If you prefer not to see this service, you will be able to hide it from Settings when it launches.

+
+
+
+
+ ) +} + diff --git a/app/account/dashboard/downloads/page.tsx b/app/account/dashboard/downloads/page.tsx new file mode 100644 index 0000000..063efaf --- /dev/null +++ b/app/account/dashboard/downloads/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { motion } from "framer-motion" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { HomeTab } from "@/components/pages/dashboard/downloads/HomeTab" +import { EmailTab } from "@/components/pages/dashboard/downloads/EmailTab" +import { PassTab } from "@/components/pages/dashboard/downloads/PassTab" +import { GitTab } from "@/components/pages/dashboard/downloads/GitTab" +import { SideMenu } from "@/components/pages/dashboard/SideMenu" + +const fadeIn = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + transition: { duration: 0.4 }, +} + +export default function DownloadCenter() { + return ( +
+ +
+
+ +

Download Center

+ + + Home + Email + Pass + Git + + + + + + + + + + + + + + +
+
+
+
+ ) +} + diff --git a/app/account/dashboard/exchange/page.tsx b/app/account/dashboard/exchange/page.tsx new file mode 100644 index 0000000..638dc34 --- /dev/null +++ b/app/account/dashboard/exchange/page.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { motion } from "framer-motion" +import { SideMenu } from "@/components/pages/dashboard/SideMenu" + +const fadeIn = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + transition: { duration: 0.4 }, +} + +export default function Exchange() { + return ( +
+ +
+
+ +

Exchange Crypto

+

Coming soon

+

If you prefer not to see this service, you will be able to hide it from Settings when it launches.

+
+
+
+
+ ) +} + diff --git a/app/account/dashboard/layout.tsx b/app/account/dashboard/layout.tsx new file mode 100644 index 0000000..6ff4fd8 --- /dev/null +++ b/app/account/dashboard/layout.tsx @@ -0,0 +1,37 @@ +import { type ReactNode } from "react" +import { ThemeProvider } from "@/components/theme-provider" +import SidebarToggle from "@/components/custom/SidebarToggle" +import { SidebarProvider } from "@/components/ui/sidebar" +import { Footer } from "@/components/pages/dashboard/Footer" +import { Header } from "@/components/pages/dashboard/Header"; + +const DashboardLayout = ({ children }: { children: ReactNode }) => { + return ( + +
+
+ +
+
+ {children} +
+
+
+
+
+ +
+
+
+
+
+ ) +} + +export default DashboardLayout + diff --git a/app/account/dashboard/page.tsx b/app/account/dashboard/page.tsx index 98a701c..8e5889b 100644 --- a/app/account/dashboard/page.tsx +++ b/app/account/dashboard/page.tsx @@ -1,14 +1,13 @@ "use client" -import Image from "next/image" -import Link from "next/link" -import { useEffect, useState } from "react" -import { Mail, GitBranch, Music, Key, CheckCircle, XCircle, User, LayoutDashboard, Settings, Briefcase, Loader } from "lucide-react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Progress } from "@/components/ui/progress" -import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider } from "@/components/ui/sidebar" +import { useState, useEffect } from "react" import { motion } from "framer-motion" -import { getCookie } from "cookies-next" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { OverviewTab } from "@/components/pages/dashboard/OverviewTab" +import { SecurityTab } from "@/components/pages/dashboard/SecurityTab" +import { ServicesTab } from "@/components/pages/dashboard/ServicesTab" +import { GitTab } from "@/components/pages/dashboard/GitTab" +import { SideMenu } from "@/components/pages/dashboard/SideMenu" const fadeIn = { initial: { opacity: 0, y: 20 }, @@ -17,42 +16,48 @@ const fadeIn = { } export default function Dashboard() { - const [diskUsage, setDiskUsage] = useState(0) - const [isGitLinked, setIsGitLinked] = useState(false) - const [gitUser, setGitUser] = useState("Unlinked") - const [gitFollowerCt, setGitFollowerCt] = useState(0) - const [gitAvatar, setGitAvatar] = useState(null) - const [gitProfileCardLoading, setGitProfileCardLoading] = useState(true) - const [gitProfileCardError, setGitProfileCardError] = useState(false) + const [dashboardState, setDashboardState] = useState({ + gitUser: "Unlinked", + gitFollowerCt: 0, + gitFollowingCt: 0, + gitAvatar: "", + gitLastLogin: "", + gitProfileCardLoading: true, + gitProfileCardError: false, + gitIsAdmin: false, + gitEmail: "", + showRunSecurityCheckBtn: true, + securityCheckBtnLoading: false, + }) useEffect(() => { const checkGitLinkStatus = async () => { try { - const key = getCookie("key") - const email = getCookie("email") - - const response = await fetch("http://localhost:3001/account/links", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ key, email, exi: true }), - }) + const response = await fetch("http://localhost:3000/api/git/user") if (response.ok) { const data = await response.json() - setIsGitLinked(data.linked || false) - setGitUser(data.user || "Unlinked") - setGitAvatar(data.avatar || null) - setGitFollowerCt(data.followers || 0) - setGitProfileCardLoading(false) + setDashboardState((prev) => ({ + ...prev, + gitUser: data.username || "Unlinked", + gitAvatar: data.avatar_url || "", + gitLastLogin: data.last_login || "Never", + gitFollowerCt: data.followers_count || 0, + gitFollowingCt: data.following_count || 0, + gitIsAdmin: data.is_admin || false, + gitEmail: data.email || "", + gitProfileCardLoading: false, + })) } else { - setGitProfileCardError(true) - console.error("Failed to fetch Git link status") + throw new Error("Failed to fetch Gitea account details"); } } catch (error) { - setGitProfileCardError(true) - console.error("Error checking Git link status:", error) + console.error("Error fetching your Gitea user data:", error); + setDashboardState((prev) => ({ + ...prev, + gitProfileCardError: true, + gitProfileCardLoading: false, + })) } } @@ -60,163 +65,36 @@ export default function Dashboard() { }, []) return ( - -
- - -
- - username -
-
- - - - - - Dashboard - - - - - - Services - - - - - - My Account - - - - - - - - - - Settings - - - - -
-
-
- -

Dashboard

-
- - - - Disk Usage - - - -

{diskUsage}% of 100GB used

-
-
-
- - - - Account Security - - -
-
- - Spam Protection -
-
- - Two-Factor Authentication -
-
-
-
-
- - - - Linked Accounts - - -
-
-
- p0ntus mail -
-
-
- LibreCloud Git -
-
- - - - - - - Services - - -
-
- - Mail -
-
- - Git -
-
- - Music -
-
- - Vaultwarden -
-
-
-
-
- - - - LibreCloud Git - - -
-
- {gitProfileCardLoading ? ( -
- -
- ) : gitAvatar ? ( - User Avatar - ) : gitProfileCardError ? ( - - ) : ( - - )} -
-
-

{gitUser}

-

{gitFollowerCt} {gitFollowerCt === 1 ? "follower" : "followers"}

-
-
-
-
-
-
- -
-
-
-
+
+ +
+
+ +

Dashboard

+ + + Overview + Security + Services + Git + + + + + + + + + + + + + + +
+
+
+
) } diff --git a/app/account/dashboard/settings/page.tsx b/app/account/dashboard/settings/page.tsx new file mode 100644 index 0000000..45da57d --- /dev/null +++ b/app/account/dashboard/settings/page.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { motion } from "framer-motion" +import { SideMenu } from "@/components/pages/dashboard/SideMenu" + +const fadeIn = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + transition: { duration: 0.4 }, +} + +export default function Settings() { + return ( +
+ +
+
+ +

Settings

+

Coming soon

+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/account/dashboard/statistics/page.tsx b/app/account/dashboard/statistics/page.tsx new file mode 100644 index 0000000..feb9741 --- /dev/null +++ b/app/account/dashboard/statistics/page.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { motion } from "framer-motion" +import { SideMenu } from "@/components/pages/dashboard/SideMenu" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; + +const fadeIn = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + transition: { duration: 0.4 }, +} + +export default function Statistics() { + return ( +
+ +
+
+ +

Statistics

+ + + Operational Costs + LibreCloud's monthly bill, for you to view. + + + Month of: February + + Server +
+ Main Server + $28.88 +
+ + Domains +
+ librecloud.cc + $3.10 +
+ + Addons +
+ +0GB Disk Space + $0.00 +
+
+ +0GB RAM + $0.00 +
+ +
+ TOTAL + $31.98 +
+
+
+
+
+
+
+ ) +} + diff --git a/app/account/dashboard/upgrades/page.tsx b/app/account/dashboard/upgrades/page.tsx new file mode 100644 index 0000000..90032dc --- /dev/null +++ b/app/account/dashboard/upgrades/page.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { motion } from "framer-motion" +import { SideMenu } from "@/components/pages/dashboard/SideMenu" + +const fadeIn = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + transition: { duration: 0.4 }, +} + +export default function Upgrades() { + return ( +
+ +
+
+ +

Upgrades

+

Coming soon

+
+
+
+
+ ) +} + diff --git a/app/account/layout.tsx b/app/account/layout.tsx index 0269fb3..94c685c 100644 --- a/app/account/layout.tsx +++ b/app/account/layout.tsx @@ -1,15 +1,10 @@ -import { Theme } from "@radix-ui/themes" -import "@radix-ui/themes/styles.css" - export default function AccountLayout({ children, }: { children: React.ReactNode }) { return ( - -
{children}
-
+
{children}
) } diff --git a/app/account/login/code/page.tsx b/app/account/login/code/page.tsx deleted file mode 100644 index 140126e..0000000 --- a/app/account/login/code/page.tsx +++ /dev/null @@ -1,118 +0,0 @@ -"use client" - -import { useState, useEffect } from "react" -import { useRouter } from "next/navigation" -import { Key } from "lucide-react" -import Cookies from "js-cookie" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Alert, AlertDescription } from "@/components/ui/alert" - -export default function Login() { - const [magicCode, setMagicCode] = useState("") - const [errorMessage, setErrorMessage] = useState("") - const [isLoading, setIsLoading] = useState(false) - const router = useRouter() - - useEffect(() => { - const validateStageTwo = async () => { - setErrorMessage("") - setIsLoading(true) - - try { - const response = await fetch('http://localhost:3001/auth/validateStageTwo', { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ email: Cookies.get("email"), stageTwoKey: Cookies.get("stageTwoKey") }), - }) - - const data = await response.json() - - if (!response.ok || !data.success) { - router.push("/account/login") - } - } catch (error) { - console.error("There was a problem with checking the status of your request:", error) - setErrorMessage("An unexpected error occurred. Please try again.") - } finally { - setIsLoading(false) - } - } - - validateStageTwo() - }, [magicCode, router]) - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setIsLoading(true) - setErrorMessage("") - - try { - const response = await fetch('http://localhost:3001/auth/validateMagicCode', { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ email: Cookies.get("email"), stageTwoKey: Cookies.get("stageTwoKey"), magicCode }), - }) - - const data = await response.json() - - if (response.ok && data.success) { - Cookies.set("key", data.key) - Cookies.remove("stageTwoKey") - router.push("/account/dashboard") - } else { - setErrorMessage(data.error || "An unknown error occurred.") - } - } catch (error) { - console.error("There was a problem with checking the magic code:", error) - setErrorMessage("An unexpected error occurred. Please try again.") - } finally { - setIsLoading(false) - } - } - - return ( -
- - - Enter Your Magic Code - Check your email for the code we sent you. - - -
-
- setMagicCode(e.target.value)} - required - /> -
- - {errorMessage && ( - - {errorMessage} - - )} -
-
-
-
- ) -} - diff --git a/app/account/login/page.tsx b/app/account/login/page.tsx index d366aa3..be29d5e 100644 --- a/app/account/login/page.tsx +++ b/app/account/login/page.tsx @@ -1,86 +1,38 @@ -"use client" - import Link from "next/link" -import { useState } from "react" -import { useRouter } from "next/navigation" -import { Mail } from "lucide-react" -import Cookies from "js-cookie" import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Alert, AlertDescription } from "@/components/ui/alert" +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { auth, signIn } from "@/auth" +import { redirect } from "next/navigation"; +import { SiAuthentik } from "react-icons/si" -export default function Login() { - const [email, setEmail] = useState("") - const [errorMessage, setErrorMessage] = useState("") - const [isLoading, setIsLoading] = useState(false) - const router = useRouter() - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setIsLoading(true) - setErrorMessage("") - - try { - const response = await fetch('http://localhost:3001/auth/login', { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ email }), - }) - - const data = await response.json() - - if (response.ok && data.success) { - Cookies.set("stageTwoKey", data.stageTwoKey) - Cookies.set("email", email) - router.push("/account/login/code") - } else { - setErrorMessage(data.error || "An unknown error occurred.") - } - } catch (error) { - console.error("There was a problem with requesting a magic code:", error) - setErrorMessage("An unexpected error occurred. Please try again.") - } finally { - setIsLoading(false) - } +export default async function Login() { + const session = await auth() + + if (session) { + return redirect("/account/dashboard") } - + return (
Log in to your account + If you still have a p0ntus mail account, select "I don't have an account" -
-
- setEmail(e.target.value)} - required - /> -
- - {errorMessage && ( - - {errorMessage} - - )}
- + I don't have an account
diff --git a/app/account/logout/page.tsx b/app/account/logout/page.tsx deleted file mode 100644 index a9e5baa..0000000 --- a/app/account/logout/page.tsx +++ /dev/null @@ -1,77 +0,0 @@ -"use client" - -import { useEffect } from "react" -import { Flex, Text, Spinner } from "@radix-ui/themes" -import { useRouter } from "next/navigation" -import Cookies from "js-cookie" - -export default function Logout() { - const router = useRouter() - - useEffect(() => { - const verifyKey = async () => { - try { - const response = await fetch('http://localhost:3001/auth/validateKey', { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ email: Cookies.get("email"), key: Cookies.get("key") }), - }) - - const data = await response.json() - - if (data.error) { - router.push('/account/login') - return false; - } - } catch (error) { - console.error("There was a problem with checking the existing key:", error); - return false; - } - } - - const logoutUser = async () => { - const keycheck = await verifyKey() - if (keycheck !== false) { - try { - const response = await fetch('http://localhost:3001/auth/logout', { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ email: Cookies.get("email"), key: Cookies.get("key") }), - }) - - const data = await response.json() - - if (data.success) { - Cookies.remove("key") - Cookies.remove("email") - router.push("/account/login") - } else if (data.error) { - console.error("There was a problem with processing the logout on the backend:", data.error); - } else { - console.error("There was a problem with processing the logout on the backend:"); - } - } catch (error) { - console.error("There was a problem with processing the logout on the backend:", error); - } - } - } - - const timer = setTimeout(() => { - logoutUser() - }, 1250) - - return () => clearTimeout(timer) - }, [router]) - - return ( - - - Logging out... - - ) -} - diff --git a/app/account/page.tsx b/app/account/page.tsx new file mode 100644 index 0000000..f712ed5 --- /dev/null +++ b/app/account/page.tsx @@ -0,0 +1,14 @@ +import { auth } from "@/auth" +import { redirect } from "next/navigation" + +export default async function Page() { + const session = await auth() + + if (session) { + redirect("/account/dashboard") + } else { + redirect("/account/login") + } + + return null +} \ No newline at end of file diff --git a/app/account/settings/page.tsx b/app/account/settings/page.tsx deleted file mode 100644 index 2f368df..0000000 --- a/app/account/settings/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -"use client" - -import { useState } from "react" -import { Flex, Text, Switch, Button } from "@radix-ui/themes" -import Sidebar from "../../components/account/Sidebar" - -export default function Settings() { - const [darkMode, setDarkMode] = useState(true) - const [notifications, setNotifications] = useState(true) - - return ( - - - - - Settings - - - - Dark Mode - - - - Enable Notifications - - - - - - - - - - ) -} - diff --git a/app/account/signup/page.tsx b/app/account/signup/page.tsx new file mode 100644 index 0000000..0bbd308 --- /dev/null +++ b/app/account/signup/page.tsx @@ -0,0 +1,365 @@ +"use client" + +import type React from "react" + +import { useState, useEffect } from "react" +import { Card, CardContent, CardHeader, CardFooter, CardTitle, CardDescription } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Button } from "@/components/ui/button" +import { Switch } from "@/components/ui/switch" +import Link from "next/link" +import { motion, AnimatePresence } from "framer-motion" +import { UserPlus, UserCog, Heart, AlertCircle, CheckCircle2, Mail, Lock, User } from "lucide-react" +import { useRouter } from "next/navigation" +import validator from "validator" +import PasswordValidator from "password-validator" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" + +export default function Signup() { + const router = useRouter() + const [formType, setFormType] = useState<"initial" | "create" | "migrate">("initial") + const [formData, setFormData] = useState({ + name: "", + emailUsername: "", + emailDomain: "librecloud.cc", + password: "", + terms: false, + migratePassword: "", + migrateTerms: false, + migrateName: "", + }) + + const [isValid, setIsValid] = useState(false) + const [validationMessage, setValidationMessage] = useState("") + const [isSubmitting, setIsSubmitting] = useState(false) + + const fadeInOut = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -20 }, + transition: { duration: 0.3 }, + } + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value, type, checked } = e.target + setFormData((prev) => ({ + ...prev, + [name]: type === "checkbox" ? checked : value, + })) + } + + useEffect(() => { + if (formType === "create") { + const { name, emailUsername, emailDomain, password, terms } = formData + const isNameValid = name.length >= 2 + const isEmailValid = validateEmail(emailUsername, emailDomain) + const isPasswordValid = validatePassword(password) + setIsValid(isNameValid && isEmailValid && isPasswordValid && terms) + + if (!isNameValid) setValidationMessage("Enter your name") + else if (!isEmailValid) setValidationMessage("Enter a valid email address") + else if (!isPasswordValid) setValidationMessage("Weak Password") + else if (!terms) setValidationMessage("Accept the terms") + else setValidationMessage("Create Account") + } else if (formType === "migrate") { + const { emailUsername, emailDomain, migratePassword, migrateTerms, migrateName } = formData + const isEmailValid = validateEmail(emailUsername, emailDomain) + const isPasswordValid = validatePassword(migratePassword) + const isNameValid = migrateName.length >= 2 + setIsValid(isEmailValid && isPasswordValid && migrateTerms && isNameValid) + + if (!isNameValid) setValidationMessage("Enter your name") + else if (!isEmailValid) setValidationMessage("Enter a valid email address") + else if (!isPasswordValid) setValidationMessage("Weak Password") + else if (!migrateTerms) setValidationMessage("Accept the terms") + else setValidationMessage("Migrate Account") + } + }, [formData, formType]) + + const getButtonIcon = () => { + if (isValid) return + if (validationMessage.includes("name")) return + if (validationMessage.includes("email")) return + if (validationMessage.includes("Password")) return + if (validationMessage.includes("terms")) return + return null + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsSubmitting(true) + + try { + const email = `${formData.emailUsername}@${formData.emailDomain}` + const response = await fetch("/api/users/create", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: formType === "create" ? formData.name : formData.migrateName, + email: email, + password: formType === "create" ? formData.password : formData.migratePassword, + migrate: formType === "migrate", + }), + }) + + const data = await response.json() + + if (data.success) { + router.push("/account/signup/success") + } else { + setValidationMessage(data.message || "Failed to create account. Please try again.") + } + } catch (error) { + console.log("[!] " + error) + setValidationMessage("An error occurred. Please contact us (see the sidebar)") + } finally { + setIsSubmitting(false) + } + } + + function validateEmail(username: string, domain: string) { + return username.length > 0 && domain.length > 0 && validator.isEmail(`${username}@${domain}`) + } + + function validatePassword(password: string) { + const passwordSchema = new PasswordValidator() + passwordSchema.is().min(8).is().max(128).has().letters().has().digits().has().not().spaces() + + return passwordSchema.validate(password) + } + + return ( +
+ + + Account Setup + Create a new account or migrate an existing one. + + + + {formType === "initial" && ( + + + + + )} + {formType === "create" && ( + +
+ + +
+
+ +
+ + @ + +
+
+
+ + +

+ Password must be 8-64 characters long, include letters and digits, and not contain spaces. +

+
+
+ setFormData((prev) => ({ ...prev, terms: checked }))} + /> + +
+
+ )} + {formType === "migrate" && ( + +
+ + +
+
+ +
+ + @ + +
+

+ A username for Authentik will be generated based on your email. Contact support if a username isn't available. +

+
+
+ + +

+ Password must be 8-64 characters long, include letters and digits, and not contain spaces. +

+
+
+ setFormData((prev) => ({ ...prev, migrateTerms: checked }))} + /> + +
+
+ )} +
+
+ + + {formType !== "initial" ? ( + + + + + ) : ( + + Welcome to the LibreCloud family! + + + )} + + +
+
+ ) +} + diff --git a/app/account/signup/success/page.tsx b/app/account/signup/success/page.tsx new file mode 100644 index 0000000..ecf7007 --- /dev/null +++ b/app/account/signup/success/page.tsx @@ -0,0 +1,160 @@ +"use client"; + +import { motion } from "framer-motion" +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { ExternalLink, Mail, Sparkles, Lock, BadgeCheck, CircleArrowRight } from "lucide-react" +import { SiGitea, SiAuthentik } from "react-icons/si" +import Link from "next/link" +import { Separator } from "@/components/ui/separator" +import { useTheme } from "next-themes" + +const services = [ + { + title: "Email", + description: "You now have access to send and receive emails with the email you created at signup.", + link: "https://mail.librecloud.cc/", + icon: Mail, + }, + { + title: "Pass", + description: "Securely store and manage your passwords across devices with Vaultwarden.", + link: "https://vaultwarden.p0ntus.com/", + icon: Lock, + }, + { + title: "Git", + description: "Host your repositories and run actions free of charge on our Gitea instance.", + link: "https://git.pontusmail.org/", + icon: SiGitea, + }, + { + title: "Authentik", + description: "A secure single-sign-on service for easy login to your other services.", + link: "https://git.pontusmail.org/", + icon: SiAuthentik, + }, +] + +const WelcomePage = () => { + const { resolvedTheme } = useTheme() + + return ( +
+
+
+
+
+ + Welcome to LibreCloud + +
+
+ +
+ + +

Your account was created

+ + + +

You've unlocked

+
+ {services.map((service, index) => ( + + + + + {service.title} + + + + {service.description} + + + + ))} +
+ + + +

Pay less, do more

+
+ +

+ + Did You Know? +

+

+ LibreCloud makes ZERO profit off upgrades, while doing the heavy lifting in the background to ensure a connected experience. +

+
+ +
+ +

Generative AI

+

+ Use GenAI tools at API pricing, without compromising on the UI. +

+
+ +

Account Upgrades

+

+ Add additional storage or services from our host at no additional fee from LibreCloud. +

+
+
+
+ + + +

Now, we set you free

+

+ Now, it's your time to explore. All we ask is you use your services in a fair way. Account bans will be enforced for abuse of our services for spamming, scamming, and all that nasty stuff. +

+

+ We live in the "Support" tab on your dashboard, and on Telegram if you need any help. Please be civil and patient, and we'll do our best to help you out. +

+

+ From here, you can proceed to sign in to your newly created account with Authentik. It will handle all the sign-ins for your account except for Pass (Vaultwarden). +

+ + + +
+
+
+ ) +} + +export default WelcomePage + diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..0eebb94 --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,2 @@ +import { handlers } from "@/auth" +export const { GET, POST } = handlers \ No newline at end of file diff --git a/app/api/git/user/route.ts b/app/api/git/user/route.ts new file mode 100644 index 0000000..68e6ae6 --- /dev/null +++ b/app/api/git/user/route.ts @@ -0,0 +1,46 @@ +import { auth } from "@/auth" +import { NextResponse } from "next/server" +import { prisma } from "@/lib/prisma" + +export async function GET() { + try { + const session = await auth() + + if (!session || !session.user?.email) { + return NextResponse.json({ error: "Unauthorized - Please login first" }, { status: 401 }) + } + + const { email } = session.user + + const dbUser = await prisma.user.findUnique({ + where: { email }, + }) + + if (!dbUser) { + return NextResponse.json({ message: "User not found in database" }) + } + + const response = await fetch(`https://git.pontusmail.org/api/v1/users/${dbUser.username}`, { + headers: { + Authorization: `Bearer ${process.env.GITEA_API_KEY}`, + "Content-Type": "application/json", + },// don't forget to smile today :) + }) + + if (!response.ok) { + return NextResponse.json({ error: "Failed to fetch Git user data" }, { status: response.status }) + } + + const userData = await response.json() + + if (userData.email !== email) { + return NextResponse.json({ error: "Email verification failed" }, { status: 403 }) + } + + return NextResponse.json(userData) + } catch (error) { + console.error("Git status API error:", error) + return NextResponse.json({ error: "Internal server error" }, { status: 500 }) + } +} + diff --git a/app/api/users/create/route.ts b/app/api/users/create/route.ts new file mode 100644 index 0000000..633497d --- /dev/null +++ b/app/api/users/create/route.ts @@ -0,0 +1,125 @@ +import { prisma } from "@/lib/prisma" +import axios from "axios" +import { NextResponse } from "next/server" + +// This endpoint has three functions: +// (1) Create a new LibreCloud user (Authentik, Email) +// (2) Link a user with their Git account via DB (creates user in db) +// (3) Migrate a p0ntus mail account to a LibreCloud account (creates/migrates Authentik/Email) + +async function createEmail(email: string, password: string, migrate: boolean) { + const response = await axios.post("http://localhost:6723/accounts/add", { + email, + password, + migrate, + }) + + const responseData = response.data + if (responseData.success) { + return response.data + } else if (responseData.error) { + return { success: false, message: responseData.error } + } else { + return { success: false, message: "Failed to create email account" } + } +} + +export async function POST(request: Request) { + try { + const body = await request.json() + const { name, username, email, password, migrate } = body + + console.log(body) + + if (!name || !email || !password) { + return NextResponse.json({ success: false, message: "Request is incomplete" }) + } + + // Insert into database if user provided Git username + if (username) { + const user = await prisma.user.create({ + data: { + email, + username, + }, + }) + return NextResponse.json(user) + } else { + // Create Authentik user + const genUser = email.split("@")[0] + const userData = { + username: genUser, + name, + is_active: true, + groups: [ + "b2c38bad-1d15-4ffd-b6d4-d95370a092ca" // this represents the "Users" group in Authentik + ], + email, + type: "internal", + } + console.log(userData) + const response = await axios.request({ + method: "post", + maxBodyLength: Infinity, + url: "https://auth.librecloud.cc/api/v3/core/users/", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Authorization: `Bearer ${process.env.AUTHENTIK_API_KEY}`, + }, + data: JSON.stringify(userData), + validateStatus: () => true, // capture response even for error status codes + }) + + if (response.data?.detail) { + console.log(response.data.detail) + } + + if (response.status !== 201) { + console.log(response.data) + if (response.data.username && response.data.username[0] === "This field must be unique.") { + return NextResponse.json({ success: false, message: "Username already exists" }) + } + return NextResponse.json({ success: false, message: "Failed to create user in Authentik" }) + } else { + const userID = response.data.pk + const updData = { + password, + } + const updCfg = await axios.request({ + method: "post", + maxBodyLength: Infinity, + url: `https://auth.librecloud.cc/api/v3/core/users/${userID}/set_password/`, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.AUTHENTIK_API_KEY}`, + }, + data: updData, + validateStatus: () => true, // capture response even for error status codes + }) + + if (updCfg.data?.detail) { + console.log(updCfg.data.detail) + } + + if (updCfg.status === 204) { + // account created successfully, now create email + const emailRes = await createEmail(email, password, migrate) + return NextResponse.json(emailRes) + } else if (updCfg.status === 400) { + return NextResponse.json({ success: false, message: "Bad Request - Failed to set Authentik password" }) + } else { + return NextResponse.json({ success: false, message: "Internal Server Error - Failed to set Authentik password" }) + } + } + } + } catch (error: unknown) { + if (axios.isAxiosError(error) && error.response?.data?.detail) { + console.log("Axios error detail:", error.response.data.detail) + } else { + console.log(error) + } + return NextResponse.json({ success: false, message: "Internal server error - Failed to create user" }) + } +} + diff --git a/app/components/FeatureCard.tsx b/app/components/FeatureCard.tsx deleted file mode 100644 index 8645215..0000000 --- a/app/components/FeatureCard.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { LucideIcon } from "lucide-react" -import { Button } from "@/components/ui/button" - -interface FeatureCardProps { - title: string - description: string - link: string - icon: LucideIcon -} - -const FeatureCard = ({ title, description, link, icon: Icon }: FeatureCardProps) => { - return ( -
-
- -
-

{title}

-

{description}

- - - -
- ) -} - -export default FeatureCard - diff --git a/app/components/Navbar.tsx b/app/components/Navbar.tsx deleted file mode 100644 index 19f12ec..0000000 --- a/app/components/Navbar.tsx +++ /dev/null @@ -1,87 +0,0 @@ -'use client'; - -import { useState } from "react" -import Link from "next/link" -import { Menu, X, Server, Home, User } from "lucide-react" - -const Navbar = () => { - const [isOpen, setIsOpen] = useState(false) - - return ( - - ) -} - -export default Navbar - diff --git a/app/components/account/Sidebar.tsx b/app/components/account/Sidebar.tsx deleted file mode 100644 index a9015a6..0000000 --- a/app/components/account/Sidebar.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import Link from "next/link" -import { Cog, LogOut, Gauge } from "lucide-react" -import { Flex, Card } from "@radix-ui/themes" -import { usePathname } from "next/navigation" - -const Sidebar = () => { - const pathname = usePathname() - - const navItems = [ - { href: "/account/dashboard", icon: Gauge, label: "Dashboard" }, - { href: "/account/settings", icon: Cog, label: "Settings" }, - { href: "/account/logout", icon: LogOut, label: "Logout" }, - ] - - return ( - - - {navItems.map((item) => ( - - - {item.label} - - ))} - - - ) -} - -export default Sidebar - diff --git a/app/globals.css b/app/globals.css index c455351..3d69d67 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,78 +2,100 @@ @tailwind components; @tailwind utilities; -body { - font-family: Arial, Helvetica, sans-serif; -} - @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --ring: 215 20.2% 65.1%; + --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; } + .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 85.7% 97.3%; + + --ring: 217.2 32.6% 17.5%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; } } @@ -85,4 +107,5 @@ body { body { @apply bg-background text-foreground; } -} \ No newline at end of file +} + diff --git a/app/layout.tsx b/app/layout.tsx index dd0321a..d3a7093 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,11 +1,8 @@ import "./globals.css" import type { Metadata } from "next" -import Head from "next/head" -import { Inter } from "next/font/google" -import Navbar from "../app/components/Navbar" -import "@radix-ui/themes/styles.css"; - -const inter = Inter({ subsets: ["latin"] }) +import { Providers } from "@/app/providers" +import type React from "react" +import { GeistSans } from 'geist/font/sans'; export const metadata: Metadata = { title: "LibreCloud", @@ -13,25 +10,15 @@ export const metadata: Metadata = { } export default function RootLayout({ - children, -}: { + children, + }: { children: React.ReactNode }) { return ( - - - {`${metadata.title}`} - - - - - - - - - {children} - + + + {children} + ) -} - +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 347271c..bc9dffa 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,12 +1,14 @@ -import Hero from "../app/components/Hero" -import FeatureCard from "../app/components/FeatureCard" -import { Mail, Lock, Code, } from "lucide-react" +import Hero from "@/components/pages/main/Hero" +import FeatureCard from "@/components/pages/main/FeatureCard" +import { Mail, Lock, Disc3 } from "lucide-react" +import { SiGitea, SiAuthentik } from "react-icons/si"; +import Navbar from "@/components/pages/main/Navbar" export default function Home() { const features = [ { title: "Email", - description: "4GB of free email storage and a synced calendar.", + description: "Free email service with webmail and antispam, powered by a custom docker-mailserver setup.", link: "https://pontusmail.org/", icon: Mail, }, @@ -17,27 +19,42 @@ export default function Home() { icon: Lock, }, { - title: "Repo Hosting", - description: "Host your code repositories on our Gitea instance.", + title: "Git", + description: "Host your repositories and run actions free of charge on our Gitea instance.", link: "https://git.pontusmail.org/", - icon: Code, + icon: SiGitea, + }, + { + title: "Authentik", + description: "A secure single-sign-on service for easy login to your other services.", + link: "https://git.pontusmail.org/", + icon: SiAuthentik, + }, + { + title: "Music", + description: "Coming soon. Host your music on our community server and stream it everywhere", + link: "https://git.pontusmail.org/", + icon: Disc3, }, ] return ( -
- -
-
-

Services

-
- {features.map((feature, index) => ( - - ))} +
+ +
+ +
+
+

Our Services

+
+ {features.map((feature, index) => ( + + ))} +
-
-
-
+ + +
) } diff --git a/app/providers.tsx b/app/providers.tsx new file mode 100644 index 0000000..aeaab0d --- /dev/null +++ b/app/providers.tsx @@ -0,0 +1,13 @@ +"use client" + +import { ThemeProvider } from "next-themes" +import type { ReactNode } from "react" + +export function Providers({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} + diff --git a/auth.ts b/auth.ts new file mode 100644 index 0000000..f336e51 --- /dev/null +++ b/auth.ts @@ -0,0 +1,14 @@ +import NextAuth from "next-auth" +import Authentik from "next-auth/providers/authentik" + +export const { handlers, signIn, signOut, auth } = NextAuth({ + providers: [Authentik], + callbacks: { + authorized: async ({ auth }) => { + return !!auth + }, + }, + pages: { + signIn: "/account/login", + }, +}) \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index fb0b7da2b27f6a9755e2466e6d35d65176c719e1..88e4e6798d0ad49aab3fc6189e75ea45ef24ae72 100755 GIT binary patch delta 61733 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

FpyzFeP;U#`tGX18wU?+nKZ#R$oKmcaiV(hSN) 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 delta 51257 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 diff --git a/components/cards/dashboard/DiskUsage.tsx b/components/cards/dashboard/DiskUsage.tsx new file mode 100644 index 0000000..4e5977f --- /dev/null +++ b/components/cards/dashboard/DiskUsage.tsx @@ -0,0 +1,32 @@ +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { HardDrive } from "lucide-react"; +import React, { useState } from "react"; + +export const DiskUsage = () => { + // TODO: Implement disk usage card logic + //const [diskUsage, setDiskUsage] = useState(0); + //const [quotaGB, setQuotaGB] = useState(0); + //const [quotaUsedGB, setQuotaUsedGB] = useState(0); + const [diskUsage] = useState(0); + const [quotaGB] = useState(0); + const [quotaUsedGB] = useState(0); + + return ( + + + Disk Usage + Your email accounts' disk quota usage + + +

+ +
+ +

{quotaUsedGB}GB of {quotaGB}GB used

+
+
+ + + ); +}; \ No newline at end of file diff --git a/components/cards/dashboard/overview/LinkedAccounts.tsx b/components/cards/dashboard/overview/LinkedAccounts.tsx new file mode 100644 index 0000000..cb58479 --- /dev/null +++ b/components/cards/dashboard/overview/LinkedAccounts.tsx @@ -0,0 +1,85 @@ +import { useState, useEffect } from "react"; +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Loader2 } from "lucide-react"; + +export const LinkedAccounts = () => { + const [gitStatus, setGitStatus] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isAdmin, setIsAdmin] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchGitStatus = async () => { + setIsLoading(true); + try { + const response = await fetch("/api/git/user") + const data = await response.json() + if (!response.ok) { + if (data.error) { + throw new Error(data.error) + } else { + throw new Error(`HTTP error: ${response.status}`) + } + } + if (data.is_admin) { + setIsAdmin(true) + } + if (!data.message) { + setGitStatus(true) + } + } catch (err: unknown) { + if (err instanceof Error) { + setError(err.message) + } + } finally { + setIsLoading(false) + } + }; + + fetchGitStatus() + }, []); + + return ( + + + Linked Accounts + LibreCloud-connected services + + +
    +
  • + {isLoading ? ( + + ) : ( + + )} + {isAdmin ? ( +
    + LibreCloud Git + + Admin + +
    + ) : ( + LibreCloud Git + )} +
  • +
  • + + p0ntus mail +
  • +
+
+ {error &&

{error}

} +
+
+
+ ); +}; \ No newline at end of file diff --git a/components/cards/dashboard/overview/WelcomeCard.tsx b/components/cards/dashboard/overview/WelcomeCard.tsx new file mode 100644 index 0000000..7be4c13 --- /dev/null +++ b/components/cards/dashboard/overview/WelcomeCard.tsx @@ -0,0 +1,45 @@ +import React, { useState, useEffect } from "react"; +import Cookies from "js-cookie"; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Check } from "lucide-react"; + +export const WelcomeCard = () => { + const [visible, setVisible] = useState(true); + + useEffect(() => { + const isRead = Cookies.get("welcome-read"); + if (isRead === "true") { + setVisible(false); + } + }, []); + + const handleMarkAsRead = () => { + Cookies.set("welcome-read", "true"); + setVisible(false); + }; + + if (!visible) return null; + + return ( + + + Welcome to your dashboard + + +

+ Thanks for logging in! Here you can manage your account and the services available to you. +

+

+ We’re thrilled to have you on board, and if you need anything, don’t hesitate to contact support (see the sidebar). +

+

That’s all, have a great day!

+
+ + + +
+ ); +}; \ No newline at end of file diff --git a/components/custom/LogoutMenuItem.tsx b/components/custom/LogoutMenuItem.tsx new file mode 100644 index 0000000..e574371 --- /dev/null +++ b/components/custom/LogoutMenuItem.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar"; +import { DoorOpen } from "lucide-react"; +import { logout } from "@/actions/logout"; + +export function SidebarSignOut() { + return ( + + + + + Logout + + + + ); +} + +export default SidebarSignOut; \ No newline at end of file diff --git a/components/custom/SidebarToggle.tsx b/components/custom/SidebarToggle.tsx new file mode 100644 index 0000000..e3fb45e --- /dev/null +++ b/components/custom/SidebarToggle.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { useSidebar } from "@/components/ui/sidebar"; +import { Button } from "@/components/ui/button"; +import { Menu } from "lucide-react"; + +const SidebarToggle = () => { + // TODO: Sidebar logic needs fixing (hide sidebar on button click) + const { toggleSidebar } = useSidebar(); + return ( +
+ +
+ ); +}; + +export default SidebarToggle; + diff --git a/components/custom/ThemeToggle.tsx b/components/custom/ThemeToggle.tsx new file mode 100644 index 0000000..9b8f781 --- /dev/null +++ b/components/custom/ThemeToggle.tsx @@ -0,0 +1,43 @@ +"use client" + +import * as React from "react" +import { Moon, Sun } from "lucide-react" +import { useTheme } from "next-themes" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +export function ThemeToggle() { + const { setTheme } = useTheme() + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ) +} \ No newline at end of file diff --git a/components/pages/dashboard/Footer.tsx b/components/pages/dashboard/Footer.tsx new file mode 100644 index 0000000..877ce64 --- /dev/null +++ b/components/pages/dashboard/Footer.tsx @@ -0,0 +1,28 @@ +"use client" + +import {useEffect, useState} from "react" + +export function Footer() { + const [renderTime, setRenderTime] = useState(null) + + useEffect(() => { + const startTime = performance.now() + requestAnimationFrame(() => { + requestAnimationFrame(() => { + const endTime = performance.now() + const timeTaken = endTime - startTime + setRenderTime(timeTaken) + }) + }) + }, []) + + return ( +
+
+

Created by a community, not a company.

+ {renderTime !== null ?

Page rendered in {renderTime.toFixed(2)} ms

:

Calculating render time...

} +
+
+ ) +} + diff --git a/components/pages/dashboard/GitTab.tsx b/components/pages/dashboard/GitTab.tsx new file mode 100644 index 0000000..a08a722 --- /dev/null +++ b/components/pages/dashboard/GitTab.tsx @@ -0,0 +1,197 @@ +"use client" + +//import { useState } from "react" +import { User, Users, Clock } from "lucide-react" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Badge } from "@/components/ui/badge" +//import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +//import { Input } from "@/components/ui/input" +//import { Label } from "@/components/ui/label" +//import { Switch } from "@/components/ui/switch" +//import { Textarea } from "@/components/ui/textarea" + +interface DashboardState { + gitUser: string; + gitAvatar?: string; + gitLastLogin?: string; + gitFollowerCt: number; + gitFollowingCt: number; + gitIsAdmin: boolean; + gitEmail?: string; +} + +export const GitTab = ({ dashboardState }: { dashboardState: DashboardState }) => { + /* + This is disabled for later, so I can finish working on it. I want to focus on essential services first. + + const [newUsername, setNewUsername] = useState("") + const [newPassword, setNewPassword] = useState("") + const [newEmail, setNewEmail] = useState("") + const [repoName, setRepoName] = useState("") + const [isPrivate, setIsPrivate] = useState(false) + const [repoDescription, setRepoDescription] = useState("") + const [autoInit, setAutoInit] = useState(true) + + const handleUsernameChange = (e: React.FormEvent) => { + e.preventDefault() + console.log("Data:", newUsername) + } + + const handlePasswordChange = (e: React.FormEvent) => { + e.preventDefault() + console.log("Changing password") + } + + const handleEmailChange = (e: React.FormEvent) => { + e.preventDefault() + console.log("Data:", newEmail) + } + + const handleCreateRepo = (e: React.FormEvent) => { + e.preventDefault() + console.log("Data:", { repoName, isPrivate, repoDescription, autoInit }) + } + */ + + const convDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleString(); + } + + return ( +
+
+ + + Profile + An overview of your LibreCloud Git account + + +
+ + + + + + +
+

+ {dashboardState.gitUser} + {dashboardState.gitIsAdmin && Admin} +

+
+ + {dashboardState.gitFollowerCt} followers + + + {dashboardState.gitFollowingCt} following + +
+
+
+
+ + Last login: {dashboardState.gitLastLogin === "Never" ? "Never" : (dashboardState.gitLastLogin && convDate(dashboardState.gitLastLogin)) || "N/A"} +
+
+
+
+ {/* + This is disabled for later, so I can finish working on it. I want to focus on essential services first. + +
+ + + Change Username + + +
+
+ + +
+
+ + setNewUsername(e.target.value)} /> +
+ +
+
+
+ + + Change Password + + +
+
+ + setNewPassword(e.target.value)} + /> +
+ +
+
+
+ + + Change Email + + +
+
+ + a + "*".repeat(b.length)) || ""} + disabled + /> +
+
+ + setNewEmail(e.target.value)} /> +
+ +
+
+
+ + + Create New Repository + + +
+
+ + setRepoName(e.target.value)} /> +
+
+ + +
+
+ +