diff --git a/package.json b/package.json index 51ebc9a..88626e3 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "react-select-search": "^3.0.9", + "sweetalert2": "^11.4.10", "tailwindcss": "^3.0.24", "typescript": "^4.6.4", "web-vitals": "^2.1.4" diff --git a/public/locales/en-US/gcauth.json b/public/locales/en-US/gcauth.json new file mode 100644 index 0000000..61a5e94 --- /dev/null +++ b/public/locales/en-US/gcauth.json @@ -0,0 +1,4 @@ +{ + "navigation": "GCAuth", + "title": "GCAuth Token Generator" +} \ No newline at end of file diff --git a/src/components/MenuDesktop.tsx b/src/components/MenuDesktop.tsx index 3a93fdb..342ffed 100644 --- a/src/components/MenuDesktop.tsx +++ b/src/components/MenuDesktop.tsx @@ -20,6 +20,12 @@ export default function MenuDesktop(props:{handleHeaderTitleChange: (title:strin ) : ( handleHeaderTitleChange("Artifact Command Generator")} className="menu-item text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">Artifact )} + + {location.pathname === "/gcauth" ? ( + GCAuth + ) : ( + handleHeaderTitleChange("GCAuth Token Generator")} className="menu-item block text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">GCAuth + )} ) } \ No newline at end of file diff --git a/src/components/MenuMobile.tsx b/src/components/MenuMobile.tsx index d81ac35..b24f03d 100644 --- a/src/components/MenuMobile.tsx +++ b/src/components/MenuMobile.tsx @@ -20,6 +20,12 @@ export default function MenuMobile(props: any) { ) : ( handleHeaderTitleChange("Artifact Command Generator")} className="menu-item block text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">Artifact )} + + {location.pathname === "/gcauth" ? ( + GCAuth + ) : ( + handleHeaderTitleChange("GCAuth Token Generator")} className="menu-item block text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">GCAuth + )} ) } \ No newline at end of file diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index def1893..4072a75 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,14 +1,20 @@ -import React from "react"; +import React, {useEffect} from "react"; import MenuDesktop from "./MenuDesktop"; import MenuMobile from "./MenuMobile"; import {Menu} from "@mui/material"; -import i18n from "i18next"; +import i18n, {use} from "i18next"; import {useTranslation} from "react-i18next"; import LanguageChange from "./LanguageChange"; +import {useNavigate} from "react-router-dom"; -class Navbar extends React.Component { - handleHeaderTitleChange; - toggleMobileMenu = () => { +interface Props { + handleHeaderTitleChange: (title: string) => void; +} +export default function Navbar(props: Props) { + const {handleHeaderTitleChange} = props; + const pathname = window.location.pathname; + const {t} = useTranslation(); + const toggleMobileMenu = () => { const menuShow = document.querySelector(".menu-show"); const menuHide = document.querySelector(".menu-hide"); const mobileMenu = document.querySelector("#mobile-menu"); @@ -28,56 +34,54 @@ class Navbar extends React.Component { } } } - - constructor(props:{handleHeaderTitleChange: (title:string) => void}) { - super(props); - const {handleHeaderTitleChange} = props; - - this.handleHeaderTitleChange = handleHeaderTitleChange; - const pathname =window.location.pathname; - - if(pathname==="/") handleHeaderTitleChange("Home") - else if(pathname==="/artifact") handleHeaderTitleChange("Artifact Command Generator") - } - - render() { - return( - + ) +} \ No newline at end of file diff --git a/src/components/RouteMenu.tsx b/src/components/RouteMenu.tsx index 6df7131..19078d8 100644 --- a/src/components/RouteMenu.tsx +++ b/src/components/RouteMenu.tsx @@ -1,6 +1,7 @@ import {Routes, Route, RouteProps} from "react-router-dom"; import React from "react"; import Artifacts from "./pages/Artifacts"; +import GCAuth from "./pages/GCAuth"; interface RouteMenuProps extends RouteProps{ handleHeaderTitleChange: (title: string) => void; @@ -17,6 +18,9 @@ export default function RouteMenu(props: RouteMenuProps) { }/> + + }/> ); } \ No newline at end of file diff --git a/src/components/pages/GCAuth.tsx b/src/components/pages/GCAuth.tsx new file mode 100644 index 0000000..da176f0 --- /dev/null +++ b/src/components/pages/GCAuth.tsx @@ -0,0 +1,322 @@ +import {FormEvent, useEffect, useState} from "react"; +import Swal from "sweetalert2"; + +interface IGCAuthResponse { + status: string; + message: string; + jwt: string; +} + +interface IJWTPayload { + token: string; + username: string; + uid: string; +} + +interface IGCAuthLogin { + username: string; + password: string; +} + +interface IGCAuthRegister { + username: string; + password: string; + password_confirmation: string; +} + +interface IGCAuthChangePassword { + username: string; + new_password: string; + new_password_confirmation: string; + old_password: string; +} + +export default function GCAuth() { + const [jwt, setJwt] = useState(""); + const [dispatchServer, setDispatchServer] = useState(localStorage.getItem("dispatchServer") ?? ""); + const [useSSl, setUseSSl] = useState(localStorage.getItem("useSSl")==="true" ?? true); + const [baseUrl, setBaseUrl] = useState(""); + + const [loginUsername, setLoginUsername] = useState(""); + const [loginPassword, setLoginPassword] = useState(""); + const [token, setToken] = useState(""); + + const [registerUsername, setRegisterUsername] = useState(""); + const [registerPassword, setRegisterPassword] = useState(""); + const [registerPasswordConfirmation, setRegisterPasswordConfirmation] = useState(""); + + const [changePasswordUsername, setChangePasswordUsername] = useState(""); + const [changePasswordNewPassword, setChangePasswordNewPassword] = useState(""); + const [changePasswordNewPasswordConfirmation, setChangePasswordNewPasswordConfirmation] = useState(""); + const [changePasswordOldPassword, setChangePasswordOldPassword] = useState(""); + + const checkGCAuth = async () => { + fetch(baseUrl + "/authentication/type") + .then(res => res.text()) + .then(res => { + if (res === "me.exzork.gcauth.handler.GCAuthAuthenticationHandler") { + Swal.fire({ + toast: true, + position: "top-end", + showConfirmButton: false, + timer: 3000, + timerProgressBar: true, + title: "GCAuth is installed", + icon: "success" + }); + } else { + Swal.fire({ + toast: true, + position: "top-end", + showConfirmButton: false, + timer: 3000, + timerProgressBar: true, + title: "GCAuth is not installed", + icon: "error" + }); + } + }) + .catch(err => { + console.log(err); + }); + } + + useEffect(() => { + setBaseUrl(`http${useSSl ? "s" : ""}://${dispatchServer}`); + localStorage.setItem("dispatchServer", dispatchServer); + localStorage.setItem("useSSl", useSSl.toString()); + }, [useSSl, dispatchServer]); + + const handleLogin = async (e: FormEvent) => { + e.preventDefault(); + const data: IGCAuthLogin = { + username: loginUsername, + password: loginPassword + } + fetch(baseUrl + "/authentication/login", {method: "POST", body: JSON.stringify(data)}) + .then(async (res) => { + const resText = await res.text(); + try { + let resJson = JSON.parse(resText); + if (resJson.success) { + await Swal.fire({ + toast: true, + position: "top-end", + showConfirmButton: false, + timer: 3000, + timerProgressBar: true, + title: "Login success", + icon: "success" + }); + setJwt(resJson.jwt); + const splitToken = resJson.jwt.split("."); + const payload = JSON.parse(atob(splitToken[1])); + setToken(payload.token); + } else { + await Swal.fire({ + title: "Login failed", + text: resJson.message ?? resJson, + icon: "error" + }); + } + } catch (e) { + await Swal.fire({ + title: "Login failed", + text: resText, + icon: "error" + }); + } + }).catch((err) => { + console.log(err); + Swal.fire({ + title: "Login failed", + text: err, + icon: "error" + }); + }); + } + + const handleRegister = async (e: FormEvent) => { + e.preventDefault(); + const data: IGCAuthRegister = { + username: registerUsername, + password: registerPassword, + password_confirmation: registerPasswordConfirmation + } + fetch(baseUrl + "/authentication/register", {method: "POST", body: JSON.stringify(data)}) + .then(async (res) => { + const resText = await res.text(); + try { + let resJson = JSON.parse(resText); + if (resJson.success) { + await Swal.fire({ + toast: true, + position: "top-end", + showConfirmButton: false, + timer: 3000, + timerProgressBar: true, + title: "Register success", + icon: "success" + }); + setJwt(resJson.jwt); + } else { + await Swal.fire({ + title: "Register failed", + text: resJson.message ?? resJson, + icon: "error" + }); + } + } catch (e) { + await Swal.fire({ + title: "Register failed", + text: resText, + icon: "error" + }); + } + }).catch((err) => { + console.log(err); + Swal.fire({ + title: "Register failed", + text: err, + icon: "error" + }); + }); + } + + const handleChangePassword = async (e: FormEvent) => { + e.preventDefault(); + const data: IGCAuthChangePassword = { + username: changePasswordUsername, + new_password: changePasswordNewPassword, + new_password_confirmation: changePasswordNewPasswordConfirmation, + old_password: changePasswordOldPassword + } + + fetch(baseUrl + "/authentication/change_password", {method: "POST", body: JSON.stringify(data)}) + .then(async (res) => { + const resText = await res.text(); + try { + let resJson = JSON.parse(resText); + if (resJson.success) { + await Swal.fire({ + toast: true, + position: "top-end", + showConfirmButton: false, + timer: 3000, + timerProgressBar: true, + title: "Change password success", + icon: "success" + }); + } else { + await Swal.fire({ + title: "Change password failed", + text: resJson.message ?? resJson, + icon: "error" + }); + } + } catch (e) { + await Swal.fire({ + title: "Change password failed", + text: resText, + icon: "error" + }); + } + }).catch((err) => { + console.log(err); + Swal.fire({ + title: "Change password failed", + text: err, + icon: "error" + }); + }); + } + + const handleCopyToken = (e:FormEvent) => { + e.preventDefault(); + if (token!==""){ + navigator.clipboard.writeText(token); + Swal.fire({toast: true, position: "top-end", showConfirmButton: false, timer: 3000, timerProgressBar: true, title: "Token copied", icon: "success"}); + }else{ + Swal.fire({title: "No token", text: "You need to login first", icon: "error"}); + } + } + + return ( + <> +
+
+
+ setDispatchServer(e.currentTarget.value)} + placeholder="Input server ip/domain with port if needed" + className="block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md"/> +
+ setUseSSl(e.currentTarget.checked)} id="with-ssl" + className="form-checkbox m-auto text-indigo-600 transition duration-150 ease-in-out"/> + +
+ +
+
+
+
+

Login

+ setLoginUsername(e.currentTarget.value)} + className="mt-2 block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md"/> + setLoginPassword(e.currentTarget.value)} + className="mt-2 block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md"/> + + +
+
+
+
+

Register

+ setRegisterUsername(e.currentTarget.value)} + className="mt-2 block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md"/> + setRegisterPassword(e.currentTarget.value)} + className="mt-2 block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md"/> + setRegisterPasswordConfirmation(e.currentTarget.value)} + className="mt-2 block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md"/> + +
+
+
+
+

Change Password

+ setChangePasswordUsername(e.currentTarget.value)} + className="mt-2 block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md"/> + setChangePasswordNewPassword(e.currentTarget.value)} + className="mt-2 block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md"/> + setChangePasswordNewPasswordConfirmation(e.currentTarget.value)} + className="mt-2 block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md"/> + setChangePasswordOldPassword(e.currentTarget.value)} + className="mt-2 block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md"/> + +
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/src/i18n.ts b/src/i18n.ts index 3c5a4cd..2e6d058 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -13,7 +13,7 @@ i18n interpolation: { escapeValue: false, // not needed for react!! }, - ns: ["artifact"], + ns: ["artifact","gcauth"], defaultNS: "", }); diff --git a/yarn.lock b/yarn.lock index ca3b6d9..dfe7806 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8341,6 +8341,11 @@ svgo@^2.7.0: picocolors "^1.0.0" stable "^0.1.8" +sweetalert2@^11.4.10: + version "11.4.10" + resolved "https://registry.yarnpkg.com/sweetalert2/-/sweetalert2-11.4.10.tgz#d0f7733a0997aad18b81c6f6b39a01c3a6cb4e21" + integrity sha512-Ms3mryLA/c5+llHNY0I9KXU8bv0N1a874gZr7xMPOiSnLD9veHZA92nZvJOkuN1hrdcR8kyj4IN8tBxlAIjVww== + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz"