feat: gitea account linking (+ bump, split components)
Some checks failed
Build and Push Nightly CI Image / build_and_push (push) Failing after 14s
Build and Push Docker Image / build_and_push (push) Failing after 13s
Some checks failed
Build and Push Nightly CI Image / build_and_push (push) Failing after 14s
Build and Push Docker Image / build_and_push (push) Failing after 13s
This commit is contained in:
parent
c8261f4a9b
commit
7c488ef038
1
.gitignore
vendored
1
.gitignore
vendored
@ -48,6 +48,7 @@ next-env.d.ts
|
||||
|
||||
# package-lock
|
||||
package-lock.json
|
||||
bun.lock
|
||||
bun.lockb
|
||||
|
||||
# prisma
|
||||
|
@ -1,4 +1,4 @@
|
||||
# <center>web</center>
|
||||
# web
|
||||
|
||||

|
||||
[](http://creativecommons.org/publicdomain/zero/1.0/)
|
||||
|
58
app/api/git/link/route.ts
Normal file
58
app/api/git/link/route.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { auth } from "@/auth"
|
||||
import { NextResponse } from "next/server"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await auth()
|
||||
const body = await request.json()
|
||||
const { username } = body
|
||||
|
||||
if (!session || !session.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized - Please login first" }, { status: 401 })
|
||||
} else if (!username || typeof username !== "string") {
|
||||
return NextResponse.json({ error: "Invalid username" }, { status: 400 })
|
||||
}
|
||||
|
||||
const { email } = session.user
|
||||
|
||||
const response = await fetch(`${process.env.GITEA_API_URL}/users/${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 || userData.login !== username) {
|
||||
return NextResponse.json({ error: "User verification failed" }, { status: 403 })
|
||||
}
|
||||
|
||||
const dbUsrCheck = await prisma.user.findUnique({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
|
||||
if (dbUsrCheck) {
|
||||
return NextResponse.json({ error: "Git account already linked" }, { status: 409 })
|
||||
} else {
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
username,
|
||||
},
|
||||
})
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Git status API error:", error)
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import axios from "axios"
|
||||
import { NextResponse } from "next/server"
|
||||
|
||||
// This endpoint has three functions:
|
||||
// This endpoint has two 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)
|
||||
// (2) 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(`${process.env.MAIL_CONNECT_API_URL}/accounts/add`, {
|
||||
@ -27,24 +25,12 @@ async function createEmail(email: string, password: string, migrate: boolean) {
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { name, username, email, password, migrate } = body
|
||||
|
||||
console.log(body)
|
||||
const { name, email, password, migrate } = 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 = {
|
||||
@ -57,7 +43,6 @@ export async function POST(request: Request) {
|
||||
email,
|
||||
type: "internal",
|
||||
}
|
||||
console.log(userData)
|
||||
const response = await axios.request({
|
||||
method: "post",
|
||||
maxBodyLength: Infinity,
|
||||
@ -76,7 +61,6 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
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" })
|
||||
}
|
||||
@ -112,7 +96,6 @@ export async function POST(request: Request) {
|
||||
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("Error:", error.response.data.detail)
|
||||
|
56
components/cards/dashboard/git/ChangeEmail.tsx
Normal file
56
components/cards/dashboard/git/ChangeEmail.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
interface ChangePasswordProps {
|
||||
gitEmail: string;
|
||||
}
|
||||
|
||||
export function ChangeEmail({ gitEmail }: ChangePasswordProps) {
|
||||
const [newEmail, setNewEmail] = useState("")
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
const handleEmailChange = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setMessage(null);
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
setMessage("Email updated successfully.");
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Change Email</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleEmailChange} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="current-email">Current Email</Label>
|
||||
<Input
|
||||
id="current-email"
|
||||
value={gitEmail}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="new-email">New Email</Label>
|
||||
<Input id="new-email" type="email" value={newEmail} onChange={(e) => setNewEmail(e.target.value)} />
|
||||
</div>
|
||||
<Button type="submit">
|
||||
{loading ? "Changing..." : "Change Email"}
|
||||
</Button>
|
||||
{message && <p className="text-sm text-center">{message}</p>}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangeEmail;
|
49
components/cards/dashboard/git/ChangePassword.tsx
Normal file
49
components/cards/dashboard/git/ChangePassword.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
export function ChangePassword() {
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
const handlePasswordChange = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setMessage(null);
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
setMessage("Password updated successfully.");
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Change Password</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handlePasswordChange} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="new-password">New Password</Label>
|
||||
<Input
|
||||
id="new-password"
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Changing..." : "Change Password"}
|
||||
</Button>
|
||||
{message && <p className="text-sm text-center">{message}</p>}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangePassword;
|
62
components/cards/dashboard/git/ChangeUsername.tsx
Normal file
62
components/cards/dashboard/git/ChangeUsername.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
interface ChangeUsernameProps {
|
||||
gitUser: string;
|
||||
}
|
||||
|
||||
export function ChangeUsername({ gitUser }: ChangeUsernameProps) {
|
||||
const [newUsername, setNewUsername] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setMessage(null);
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
setMessage("Username updated successfully.");
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Change Username</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="new-username">Old Username</Label>
|
||||
<Input
|
||||
id="old-username"
|
||||
type="text"
|
||||
value={gitUser}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="new-username">New Username</Label>
|
||||
<Input
|
||||
id="new-username"
|
||||
type="text"
|
||||
value={newUsername}
|
||||
onChange={(e) => setNewUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Updating..." : "Update Username"}
|
||||
</Button>
|
||||
{message && <p className="text-sm text-center">{message}</p>}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangeUsername;
|
63
components/cards/dashboard/git/CreateRepo.tsx
Normal file
63
components/cards/dashboard/git/CreateRepo.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
export function CreateRepo() {
|
||||
const [repoName, setRepoName] = useState("")
|
||||
const [isPrivate, setIsPrivate] = useState(false)
|
||||
const [repoDescription, setRepoDescription] = useState("")
|
||||
const [autoInit, setAutoInit] = useState(true)
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
const handleCreateRepo = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
setLoading(true);
|
||||
setMessage(null);
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
setMessage("Created repo!");
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Create New Repository</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleCreateRepo} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="repo-name">Repository Name</Label>
|
||||
<Input id="repo-name" value={repoName} onChange={(e) => setRepoName(e.target.value)} />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="repo-private" checked={isPrivate} onCheckedChange={setIsPrivate} />
|
||||
<Label htmlFor="repo-private">Private Repository</Label>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="repo-description">Description</Label>
|
||||
<Textarea
|
||||
id="repo-description"
|
||||
value={repoDescription}
|
||||
onChange={(e) => setRepoDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="repo-autoinit" checked={autoInit} onCheckedChange={setAutoInit} />
|
||||
<Label htmlFor="repo-autoinit">Initialize with README</Label>
|
||||
</div>
|
||||
<Button type="submit">
|
||||
{loading ? "Creating..." : "Create Repository"}
|
||||
</Button>
|
||||
{message && <p className="text-sm text-center">{message}</p>}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
60
components/cards/dashboard/git/GiteaProfileCard.tsx
Normal file
60
components/cards/dashboard/git/GiteaProfileCard.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Users, Clock, User } from "lucide-react";
|
||||
|
||||
interface DashboardState {
|
||||
gitUser: string;
|
||||
gitAvatar?: string;
|
||||
gitLastLogin?: string;
|
||||
gitFollowerCt: number;
|
||||
gitFollowingCt: number;
|
||||
gitIsAdmin: boolean;
|
||||
gitEmail?: string;
|
||||
}
|
||||
|
||||
export function GiteaProfileCard({ dashboardState }: { dashboardState: DashboardState }) {
|
||||
const convDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profile</CardTitle>
|
||||
<CardDescription>An overview of your LibreCloud Git account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar className="w-20 h-20">
|
||||
<AvatarImage src={dashboardState.gitAvatar || ""} />
|
||||
<AvatarFallback>
|
||||
<User className="w-10 h-10" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold flex items-center">
|
||||
{dashboardState.gitUser}
|
||||
{dashboardState.gitIsAdmin && <Badge className="ml-2">Admin</Badge>}
|
||||
</h3>
|
||||
<div className="flex space-x-4 text-sm text-muted-foreground">
|
||||
<span className="flex items-center">
|
||||
<Users className="w-4 h-4 mr-1" /> {dashboardState.gitFollowerCt} followers
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<Users className="w-4 h-4 mr-1" /> {dashboardState.gitFollowingCt} following
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>Last login: {dashboardState.gitLastLogin === "Never" ? "Never" : (dashboardState.gitLastLogin && convDate(dashboardState.gitLastLogin)) || "N/A"}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
121
components/cards/dashboard/git/LinkGitea.tsx
Normal file
121
components/cards/dashboard/git/LinkGitea.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { SiGitea } from "react-icons/si"
|
||||
import { Loader } from "lucide-react"
|
||||
|
||||
const giteaFormSchema = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.min(2, { message: "Username must be at least 2 characters" })
|
||||
.max(39, { message: "Username cannot exceed 39 characters" })
|
||||
.regex(/^[a-zA-Z][a-zA-Z0-9-_]*[a-zA-Z0-9]$/, {
|
||||
message: "Username must start with a letter and can only contain letters, numbers, hyphens, and underscores",
|
||||
})
|
||||
.regex(/^(?!.*[-_]{2})/, {
|
||||
message: "Username cannot contain consecutive hyphens or underscores",
|
||||
}),
|
||||
})
|
||||
|
||||
type GiteaFormValues = z.infer<typeof giteaFormSchema>
|
||||
|
||||
export function LinkGitea({ linked }: { linked: boolean }) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const form = useForm<GiteaFormValues>({
|
||||
resolver: zodResolver(giteaFormSchema),
|
||||
defaultValues: {
|
||||
username: "",
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = async (data: GiteaFormValues) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch("/api/git/link", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ username: data.username }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to link Gitea account")
|
||||
}
|
||||
|
||||
const responseData = await response.json()
|
||||
if (responseData.success) {
|
||||
console.log("Gitea account linked:", responseData)
|
||||
location.reload()
|
||||
} else if (responseData.error) {
|
||||
form.setError("username", { message: responseData.error })
|
||||
setLoading(false)
|
||||
} else {
|
||||
form.setError("username", { message: "Failed to link" })
|
||||
setLoading(false)
|
||||
throw new Error("Failed to link Gitea account")
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false)
|
||||
console.error("Error linking Gitea account:", error)
|
||||
}
|
||||
}
|
||||
|
||||
if (!linked) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Link Gitea Account
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
To link your Gitea account to your LibreCloud account, add your p0ntus mail account to your Gitea account, then click the button.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gitea Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{loading ? (
|
||||
<Button disabled>
|
||||
<Loader className="animate-spin" />
|
||||
Linking...
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="submit">
|
||||
<SiGitea />
|
||||
Link with Gitea
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,9 @@
|
||||
"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"
|
||||
import { GiteaProfileCard } from "@/components/cards/dashboard/git/GiteaProfileCard"
|
||||
import { LinkGitea } from "@/components/cards/dashboard/git/LinkGitea"
|
||||
//import { CreateRepo } from "@/components/cards/dashboard/git/CreateRepo"
|
||||
//import { ChangeUsername } from "@/components/cards/dashboard/git/ChangeUsername"
|
||||
//import { ChangePassword } from "@/components/cards/dashboard/git/ChangePassword"
|
||||
//import { ChangeEmail } from "@/components/cards/dashboard/git/ChangeEmail"
|
||||
|
||||
interface DashboardState {
|
||||
gitUser: string;
|
||||
@ -22,175 +16,21 @@ interface DashboardState {
|
||||
}
|
||||
|
||||
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<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
console.log("Data:", newUsername)
|
||||
}
|
||||
|
||||
const handlePasswordChange = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
console.log("Changing password")
|
||||
}
|
||||
|
||||
const handleEmailChange = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
console.log("Data:", newEmail)
|
||||
}
|
||||
|
||||
const handleCreateRepo = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
console.log("Data:", { repoName, isPrivate, repoDescription, autoInit })
|
||||
}
|
||||
*/
|
||||
|
||||
const convDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profile</CardTitle>
|
||||
<CardDescription>An overview of your LibreCloud Git account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar className="w-20 h-20">
|
||||
<AvatarImage src={dashboardState.gitAvatar || "/placeholder.svg"} />
|
||||
<AvatarFallback>
|
||||
<User className="w-10 h-10" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold flex items-center">
|
||||
{dashboardState.gitUser}
|
||||
{dashboardState.gitIsAdmin && <Badge className="ml-2">Admin</Badge>}
|
||||
</h3>
|
||||
<div className="flex space-x-4 text-sm text-muted-foreground">
|
||||
<span className="flex items-center">
|
||||
<Users className="w-4 h-4 mr-1" /> {dashboardState.gitFollowerCt} followers
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<Users className="w-4 h-4 mr-1" /> {dashboardState.gitFollowingCt} following
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>Last login: {dashboardState.gitLastLogin === "Never" ? "Never" : (dashboardState.gitLastLogin && convDate(dashboardState.gitLastLogin)) || "N/A"}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{(dashboardState.gitUser && dashboardState.gitUser !== "Unlinked") && (
|
||||
<GiteaProfileCard dashboardState={dashboardState} />
|
||||
)}
|
||||
<LinkGitea linked={!!dashboardState.gitUser && dashboardState.gitUser !== "Unlinked"} />
|
||||
{/*
|
||||
This is disabled for later, so I can finish working on it. I want to focus on essential services first.
|
||||
|
||||
<div className="grid gap-6 mt-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Change Username</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleUsernameChange} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="current-username">Current Username</Label>
|
||||
<Input id="current-username" value={dashboardState.gitUser} disabled />
|
||||
~-~-~-~-~-~-~-~-~-~-~-~DISABLED FOR NOW~-~-~-~-~-~-~-~-~-~-~-~
|
||||
<ChangeUsername gitUser={dashboardState.gitUser} />
|
||||
<ChangePassword />
|
||||
<ChangeEmail gitEmail={dashboardState.gitEmail || ""} />
|
||||
<CreateRepo />
|
||||
*/}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="new-username">New Username</Label>
|
||||
<Input id="new-username" value={newUsername} onChange={(e) => setNewUsername(e.target.value)} />
|
||||
</div>
|
||||
<Button type="submit">Change Username</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Change Password</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handlePasswordChange} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="new-password">New Password</Label>
|
||||
<Input
|
||||
id="new-password"
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit">Change Password</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Change Email</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleEmailChange} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="current-email">Current Email</Label>
|
||||
<Input
|
||||
id="current-email"
|
||||
value={dashboardState.gitEmail?.replace(/(.{2})(.*)(?=@)/, (_, a, b) => a + "*".repeat(b.length)) || ""}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="new-email">New Email</Label>
|
||||
<Input id="new-email" type="email" value={newEmail} onChange={(e) => setNewEmail(e.target.value)} />
|
||||
</div>
|
||||
<Button type="submit">Change Email</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Create New Repository</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleCreateRepo} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="repo-name">Repository Name</Label>
|
||||
<Input id="repo-name" value={repoName} onChange={(e) => setRepoName(e.target.value)} />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="repo-private" checked={isPrivate} onCheckedChange={setIsPrivate} />
|
||||
<Label htmlFor="repo-private">Private Repository</Label>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="repo-description">Description</Label>
|
||||
<Textarea
|
||||
id="repo-description"
|
||||
value={repoDescription}
|
||||
onChange={(e) => setRepoDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="repo-autoinit" checked={autoInit} onCheckedChange={setAutoInit} />
|
||||
<Label htmlFor="repo-autoinit">Initialize with README</Label>
|
||||
</div>
|
||||
<Button type="submit">Create Repository</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>*/}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
16
package.json
16
package.json
@ -10,7 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@prisma/client": "^6.3.1",
|
||||
"@prisma/client": "^6.4.0",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
@ -29,19 +29,19 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.0",
|
||||
"cookies-next": "^5.1.0",
|
||||
"framer-motion": "^12.4.3",
|
||||
"framer-motion": "^12.4.5",
|
||||
"geist": "^1.3.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.474.0",
|
||||
"next": "^15.2.0-canary.63",
|
||||
"next": "^15.2.0-canary.66",
|
||||
"next-auth": "^5.0.0-beta.25",
|
||||
"next-themes": "^0.4.4",
|
||||
"password-validator": "^5.3.0",
|
||||
"prisma": "^6.3.1",
|
||||
"prisma": "^6.4.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-typed": "^2.0.12",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"validator": "^13.12.0",
|
||||
@ -49,7 +49,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@tailwindcss/postcss": "^4.0.6",
|
||||
"@tailwindcss/postcss": "^4.0.7",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^20.17.19",
|
||||
"@types/react": "^19.0.10",
|
||||
@ -57,8 +57,8 @@
|
||||
"@types/validator": "^13.12.2",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"postcss": "^8.5.2",
|
||||
"tailwindcss": "^4.0.6",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.0.7",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user