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
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
bun.lock
|
||||||
bun.lockb
|
bun.lockb
|
||||||
|
|
||||||
# prisma
|
# prisma
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# <center>web</center>
|
# web
|
||||||
|
|
||||||

|

|
||||||
[](http://creativecommons.org/publicdomain/zero/1.0/)
|
[](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 axios from "axios"
|
||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
|
|
||||||
// This endpoint has three functions:
|
// This endpoint has two functions:
|
||||||
// (1) Create a new LibreCloud user (Authentik, Email)
|
// (1) Create a new LibreCloud user (Authentik, Email)
|
||||||
// (2) Link a user with their Git account via DB (creates user in db)
|
// (2) Migrate a p0ntus mail account to a LibreCloud account (creates/migrates Authentik/Email)
|
||||||
// (3) Migrate a p0ntus mail account to a LibreCloud account (creates/migrates Authentik/Email)
|
|
||||||
|
|
||||||
async function createEmail(email: string, password: string, migrate: boolean) {
|
async function createEmail(email: string, password: string, migrate: boolean) {
|
||||||
const response = await axios.post(`${process.env.MAIL_CONNECT_API_URL}/accounts/add`, {
|
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) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { name, username, email, password, migrate } = body
|
const { name, email, password, migrate } = body
|
||||||
|
|
||||||
console.log(body)
|
|
||||||
|
|
||||||
if (!name || !email || !password) {
|
if (!name || !email || !password) {
|
||||||
return NextResponse.json({ success: false, message: "Request is incomplete" })
|
return NextResponse.json({ success: false, message: "Request is incomplete" })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert into database if user provided Git username
|
// Create Authentik user
|
||||||
if (username) {
|
const genUser = email.split("@")[0]
|
||||||
const user = await prisma.user.create({
|
const userData = {
|
||||||
data: {
|
username: genUser,
|
||||||
email,
|
name,
|
||||||
username,
|
is_active: true,
|
||||||
},
|
groups: [
|
||||||
})
|
"b2c38bad-1d15-4ffd-b6d4-d95370a092ca" // this represents the "Users" group in Authentik
|
||||||
return NextResponse.json(user)
|
],
|
||||||
} else {
|
email,
|
||||||
// Create Authentik user
|
type: "internal",
|
||||||
const genUser = email.split("@")[0]
|
}
|
||||||
const userData = {
|
const response = await axios.request({
|
||||||
username: genUser,
|
method: "post",
|
||||||
name,
|
maxBodyLength: Infinity,
|
||||||
is_active: true,
|
url: `${process.env.AUTHENTIK_API_URL}/core/users/`,
|
||||||
groups: [
|
headers: {
|
||||||
"b2c38bad-1d15-4ffd-b6d4-d95370a092ca" // this represents the "Users" group in Authentik
|
"Content-Type": "application/json",
|
||||||
],
|
Accept: "application/json",
|
||||||
email,
|
Authorization: `Bearer ${process.env.AUTHENTIK_API_KEY}`,
|
||||||
type: "internal",
|
},
|
||||||
|
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)
|
return NextResponse.json({ success: false, message: "Failed to create user in Authentik" })
|
||||||
const response = await axios.request({
|
} else {
|
||||||
|
const userID = response.data.pk
|
||||||
|
const updData = {
|
||||||
|
password,
|
||||||
|
}
|
||||||
|
const updCfg = await axios.request({
|
||||||
method: "post",
|
method: "post",
|
||||||
maxBodyLength: Infinity,
|
maxBodyLength: Infinity,
|
||||||
url: `${process.env.AUTHENTIK_API_URL}/core/users/`,
|
url: `${process.env.AUTHENTIK_API_URL}/core/users/${userID}/set_password/`,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Accept: "application/json",
|
|
||||||
Authorization: `Bearer ${process.env.AUTHENTIK_API_KEY}`,
|
Authorization: `Bearer ${process.env.AUTHENTIK_API_KEY}`,
|
||||||
},
|
},
|
||||||
data: JSON.stringify(userData),
|
data: updData,
|
||||||
validateStatus: () => true, // capture response even for error status codes
|
validateStatus: () => true, // capture response even for error status codes
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.data?.detail) {
|
if (updCfg.data?.detail) {
|
||||||
console.log(response.data.detail)
|
console.log(updCfg.data.detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status !== 201) {
|
if (updCfg.status === 204) {
|
||||||
console.log(response.data)
|
// account created successfully, now create email
|
||||||
if (response.data.username && response.data.username[0] === "This field must be unique.") {
|
const emailRes = await createEmail(email, password, migrate)
|
||||||
return NextResponse.json({ success: false, message: "Username already exists" })
|
return NextResponse.json(emailRes)
|
||||||
}
|
} else if (updCfg.status === 400) {
|
||||||
return NextResponse.json({ success: false, message: "Failed to create user in Authentik" })
|
return NextResponse.json({ success: false, message: "Bad Request - Failed to set Authentik password" })
|
||||||
} else {
|
} else {
|
||||||
const userID = response.data.pk
|
return NextResponse.json({ success: false, message: "Internal Server Error - Failed to set Authentik password" })
|
||||||
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" })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
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 { GiteaProfileCard } from "@/components/cards/dashboard/git/GiteaProfileCard"
|
||||||
|
import { LinkGitea } from "@/components/cards/dashboard/git/LinkGitea"
|
||||||
//import { useState } from "react"
|
//import { CreateRepo } from "@/components/cards/dashboard/git/CreateRepo"
|
||||||
import { User, Users, Clock } from "lucide-react"
|
//import { ChangeUsername } from "@/components/cards/dashboard/git/ChangeUsername"
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
//import { ChangePassword } from "@/components/cards/dashboard/git/ChangePassword"
|
||||||
import { Badge } from "@/components/ui/badge"
|
//import { ChangeEmail } from "@/components/cards/dashboard/git/ChangeEmail"
|
||||||
//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 {
|
interface DashboardState {
|
||||||
gitUser: string;
|
gitUser: string;
|
||||||
@ -22,175 +16,21 @@ interface DashboardState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const GitTab = ({ dashboardState }: { dashboardState: 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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="space-y-6">
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<Card>
|
{(dashboardState.gitUser && dashboardState.gitUser !== "Unlinked") && (
|
||||||
<CardHeader>
|
<GiteaProfileCard dashboardState={dashboardState} />
|
||||||
<CardTitle>Profile</CardTitle>
|
)}
|
||||||
<CardDescription>An overview of your LibreCloud Git account</CardDescription>
|
<LinkGitea linked={!!dashboardState.gitUser && dashboardState.gitUser !== "Unlinked"} />
|
||||||
</CardHeader>
|
{/*
|
||||||
<CardContent className="space-y-4">
|
~-~-~-~-~-~-~-~-~-~-~-~DISABLED FOR NOW~-~-~-~-~-~-~-~-~-~-~-~
|
||||||
<div className="flex items-center space-x-4">
|
<ChangeUsername gitUser={dashboardState.gitUser} />
|
||||||
<Avatar className="w-20 h-20">
|
<ChangePassword />
|
||||||
<AvatarImage src={dashboardState.gitAvatar || "/placeholder.svg"} />
|
<ChangeEmail gitEmail={dashboardState.gitEmail || ""} />
|
||||||
<AvatarFallback>
|
<CreateRepo />
|
||||||
<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>
|
||||||
{/*
|
|
||||||
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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
16
package.json
16
package.json
@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@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-avatar": "^1.1.3",
|
||||||
"@radix-ui/react-collapsible": "^1.1.3",
|
"@radix-ui/react-collapsible": "^1.1.3",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||||
@ -29,19 +29,19 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.0",
|
"cmdk": "1.0.0",
|
||||||
"cookies-next": "^5.1.0",
|
"cookies-next": "^5.1.0",
|
||||||
"framer-motion": "^12.4.3",
|
"framer-motion": "^12.4.5",
|
||||||
"geist": "^1.3.1",
|
"geist": "^1.3.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.474.0",
|
"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-auth": "^5.0.0-beta.25",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"password-validator": "^5.3.0",
|
"password-validator": "^5.3.0",
|
||||||
"prisma": "^6.3.1",
|
"prisma": "^6.4.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-typed": "^2.0.12",
|
"react-typed": "^2.0.12",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"validator": "^13.12.0",
|
"validator": "^13.12.0",
|
||||||
@ -49,7 +49,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@tailwindcss/postcss": "^4.0.6",
|
"@tailwindcss/postcss": "^4.0.7",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^20.17.19",
|
"@types/node": "^20.17.19",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
@ -57,8 +57,8 @@
|
|||||||
"@types/validator": "^13.12.2",
|
"@types/validator": "^13.12.2",
|
||||||
"eslint": "^9.20.1",
|
"eslint": "^9.20.1",
|
||||||
"eslint-config-next": "15.1.6",
|
"eslint-config-next": "15.1.6",
|
||||||
"postcss": "^8.5.2",
|
"postcss": "^8.5.3",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.7",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user