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

This commit is contained in:
Aidan 2025-02-19 21:27:53 -05:00
parent c8261f4a9b
commit 7c488ef038
12 changed files with 550 additions and 257 deletions

1
.gitignore vendored
View File

@ -48,6 +48,7 @@ next-env.d.ts
# package-lock
package-lock.json
bun.lock
bun.lockb
# prisma

View File

@ -1,4 +1,4 @@
# <center>web</center>
# web
![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/)

58
app/api/git/link/route.ts Normal file
View 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 })
}
}

View File

@ -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,90 +25,75 @@ 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 = {
username: genUser,
name,
is_active: true,
groups: [
"b2c38bad-1d15-4ffd-b6d4-d95370a092ca" // this represents the "Users" group in Authentik
],
email,
type: "internal",
// 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",
}
const response = await axios.request({
method: "post",
maxBodyLength: Infinity,
url: `${process.env.AUTHENTIK_API_URL}/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) {
if (response.data.username && response.data.username[0] === "This field must be unique.") {
return NextResponse.json({ success: false, message: "Username already exists" })
}
console.log(userData)
const response = await axios.request({
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: `${process.env.AUTHENTIK_API_URL}/core/users/`,
url: `${process.env.AUTHENTIK_API_URL}/core/users/${userID}/set_password/`,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${process.env.AUTHENTIK_API_KEY}`,
},
data: JSON.stringify(userData),
data: updData,
validateStatus: () => true, // capture response even for error status codes
})
if (response.data?.detail) {
console.log(response.data.detail)
if (updCfg.data?.detail) {
console.log(updCfg.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" })
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 {
const userID = response.data.pk
const updData = {
password,
}
const updCfg = await axios.request({
method: "post",
maxBodyLength: Infinity,
url: `${process.env.AUTHENTIK_API_URL}/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" })
}
return NextResponse.json({ success: false, message: "Internal Server Error - Failed to set Authentik password" })
}
}
} catch (error: unknown) {

View 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;

View 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;

View 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;

View 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>
)
}

View 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>
)
}

View 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
}
}

View File

@ -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 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"} />
{/*
~-~-~-~-~-~-~-~-~-~-~-~DISABLED FOR NOW~-~-~-~-~-~-~-~-~-~-~-~
<ChangeUsername gitUser={dashboardState.gitUser} />
<ChangePassword />
<ChangeEmail gitEmail={dashboardState.gitEmail || ""} />
<CreateRepo />
*/}
</div>
{/*
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 />
</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>
)
}

View File

@ -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"
}
}