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 fb0b7da..88e4e67 100755 Binary files a/bun.lockb and b/bun.lockb differ 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)} /> +
+
+ + +
+
+ +