feat: add dashboard

This commit is contained in:
Project_IO 2025-03-19 16:27:34 +09:00
parent 7c487f6f1b
commit ad8acdbf15
8 changed files with 296 additions and 40 deletions

88
internal/routes/auth.go Normal file
View file

@ -0,0 +1,88 @@
package routes
import (
"fmt"
"os"
"git.wh64.net/devproje/kuma-archive/internal/service"
"github.com/gin-gonic/gin"
)
func authentication(group *gin.RouterGroup) {
group.POST("/login", func(ctx *gin.Context) {
auth := service.NewAuthService()
username := ctx.PostForm("username")
password := ctx.PostForm("password")
acc, err := auth.Read(username)
if err != nil {
ctx.JSON(401, gin.H{
"ok": 0,
"errno": "username or password not invalid",
})
return
}
ok, err := auth.Verify(username, password)
if err != nil || !ok {
ctx.JSON(401, gin.H{
"ok": 0,
"errno": "username or password not invalid",
})
return
}
ctx.JSON(200, gin.H{
"ok": 1,
"token": auth.Token(acc.Username, acc.Password),
})
})
group.PATCH("/update", func(ctx *gin.Context) {
auth := service.NewAuthService()
old := ctx.PostForm("password")
new := ctx.PostForm("new_password")
username, _, ok := ctx.Request.BasicAuth()
if !ok {
ctx.Status(403)
return
}
ok, err := auth.Verify(username, old)
if err != nil || !ok {
ctx.Status(403)
return
}
if err = auth.Update(username, new); err != nil {
ctx.Status(500)
_, _ = fmt.Fprintln(os.Stderr, err)
return
}
ctx.Status(200)
})
group.GET("/check", func(ctx *gin.Context) {
auth := service.NewAuthService()
username, password, ok := ctx.Request.BasicAuth()
if !ok {
ctx.Status(401)
return
}
validate, err := auth.VerifyToken(username, password)
if err != nil {
ctx.Status(500)
fmt.Fprintln(os.Stderr, err)
return
}
if !validate {
ctx.Status(401)
return
}
ctx.Status(200)
})
}

View file

@ -89,36 +89,7 @@ func New(app *gin.Engine, version *service.Version, apiOnly bool) {
}) })
auth := api.Group("/auth") auth := api.Group("/auth")
{ authentication(auth)
auth.POST("/login", func(ctx *gin.Context) {
auth := service.NewAuthService()
username := ctx.PostForm("username")
password := ctx.PostForm("password")
acc, err := auth.Read(username)
if err != nil {
ctx.JSON(401, gin.H{
"ok": 0,
"errno": "username or password not invalid",
})
return
}
ok, err := auth.Verify(username, password)
if err != nil || !ok {
ctx.JSON(401, gin.H{
"ok": 0,
"errno": "username or password not invalid",
})
return
}
ctx.JSON(200, gin.H{
"ok": 1,
"token": auth.Token(acc.Username, acc.Password),
})
})
}
api.GET("/version", func(ctx *gin.Context) { api.GET("/version", func(ctx *gin.Context) {
ctx.String(200, "%s", version.String()) ctx.String(200, "%s", version.String())
@ -139,5 +110,4 @@ func New(app *gin.Engine, version *service.Version, apiOnly bool) {
app.GET("favicon.ico", func(ctx *gin.Context) { app.GET("favicon.ico", func(ctx *gin.Context) {
ctx.File("/web/assets/favicon.ico") ctx.File("/web/assets/favicon.ico")
}) })
} }

View file

@ -146,6 +146,19 @@ func (s *AuthService) Verify(username, password string) (bool, error) {
return false, nil return false, nil
} }
func (s *AuthService) VerifyToken(username, encryptPw string) (bool, error) {
account, err := s.Read(username)
if err != nil {
return false, err
}
if encryptPw == account.Password {
return true, nil
}
return false, nil
}
func (s *AuthService) Token(username, password string) string { func (s *AuthService) Token(username, password string) string {
raw := fmt.Sprintf("%s:%s", username, password) raw := fmt.Sprintf("%s:%s", username, password)
return base64.StdEncoding.EncodeToString([]byte(raw)) return base64.StdEncoding.EncodeToString([]byte(raw))

View file

@ -12,6 +12,7 @@ import NotFound from "./components/notfound";
import Login from "./components/login"; import Login from "./components/login";
import { useAuthStore } from "./store/auth"; import { useAuthStore } from "./store/auth";
import Logout from "./components/logout"; import Logout from "./components/logout";
import Settings from "./components/settings";
function App() { function App() {
return ( return (
@ -19,6 +20,7 @@ function App() {
<Routes> <Routes>
<Route path="/login" element={<Dashboard children={<Login />} />} /> <Route path="/login" element={<Dashboard children={<Login />} />} />
<Route path="/logout" element={<Logout />} /> <Route path="/logout" element={<Logout />} />
<Route path="/settings" element={<Dashboard children={<Settings />} />} />
<Route path={"*"} element={<Dashboard children={<View />} />} /> <Route path={"*"} element={<Dashboard children={<View />} />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
@ -67,6 +69,18 @@ function View() {
function Header() { function Header() {
const auth = useAuthStore(); const auth = useAuthStore();
const [isAuth, setAuth] = useState(false);
useEffect(() => {
if (auth.token === null) {
return;
}
auth.checkToken(auth.token).then((ok) => {
if (ok)
setAuth(true);
});
}, [auth, isAuth]);
return ( return (
<nav className="ka-nav"> <nav className="ka-nav">
@ -83,17 +97,22 @@ function Header() {
<DynamicIcon name="globe" size={15} /> <DynamicIcon name="globe" size={15} />
</a> </a>
{!auth.token ? ( {!isAuth ? (
<a className="login-btn" href="/login"> <a className="login-btn" href="/login">
Login Login
</a> </a>
) : ( ) : (
<>
<a className="link" href="/settings">
<DynamicIcon name="settings" size={15} />
</a>
<div className="login-info"> <div className="login-info">
<span>Logged in as Admin</span> <span>Logged in as Admin</span>
<a className="login-btn" href="/logout"> <a className="login-btn" href="/logout">
Logout Logout
</a> </a>
</div> </div>
</>
)} )}
</div> </div>

View file

@ -13,8 +13,14 @@ function Login() {
const passwordRef = useRef<HTMLInputElement>(null); const passwordRef = useRef<HTMLInputElement>(null);
const errnoRef = useRef<HTMLInputElement>(null); const errnoRef = useRef<HTMLInputElement>(null);
if (auth.token !== null) if (auth.token !== null) {
auth.checkToken(auth.token).then((ok) => {
if (!ok)
return;
document.location.href = "/"; document.location.href = "/";
});
}
return ( return (
<div className="ka-login"> <div className="ka-login">

View file

@ -0,0 +1,109 @@
import React, { useEffect, useRef, useState } from "react";
import { AuthState, useAuthStore } from "../../store/auth";
import "./settings.scss";
function Settings() {
const auth = useAuthStore();
const [load, setLoad] = useState(false);
useEffect(() => {
if (auth.token === null) {
document.location.href = "/";
return;
}
auth.checkToken(auth.token).then((ok) => {
if (!ok) {
document.location.href = "/";
return;
}
setLoad(true);
});
}, [auth, load]);
if (!load) {
return (
<></>
);
}
return (
<div className="ka-settings">
<h2>General</h2>
<ChangePassword auth={auth} />
</div>
);
}
function SettingBox({ children }: { children: React.ReactNode }) {
return (
<div className="setting-box">
{children}
</div>
);
}
function ChangePassword({ auth }: { auth: AuthState }) {
const orRef = useRef<HTMLInputElement>(null);
const pwRef = useRef<HTMLInputElement>(null);
const ckRef = useRef<HTMLInputElement>(null);
return (
<SettingBox>
<h4>Change Password</h4>
<span>If you change your password, you will need to log in again.</span>
<hr className="line" />
<form className="box-col" id="pw-change">
<input type="password" ref={orRef} placeholder="Password" required />
<input type="password" ref={pwRef} placeholder="New Password" required />
<input type="password" ref={ckRef} placeholder="Check Password" required />
<button type="submit" className="danger" onClick={ev => {
ev.preventDefault();
const origin = orRef.current?.value;
const password = pwRef.current?.value;
const check = ckRef.current?.value;
if (!origin || !password || !check) {
return;
}
if (origin === "" || password === "" || check === "") {
alert("You must need to write all inputs");
return;
}
if (password !== check) {
alert("New password is not matches!");
return;
}
const form = new URLSearchParams();
form.append("password", origin);
form.append("new_password", password);
fetch("/api/auth/update", {
body: form,
method: "PATCH",
headers: {
"Authorization": `Basic ${auth.token}`
}
}).then((res) => {
if (res.status !== 200) {
alert(`${res.status} ${res.statusText}`);
return;
}
alert("password changed!");
document.location.href = "/logout";
});
}}>Change Password</button>
</form>
</SettingBox>
);
}
export default Settings;

View file

@ -0,0 +1,41 @@
.ka-settings {
width: 100%;
height: 100%;
display: flex;
margin: 1rem 0;
flex-direction: column;
.setting-box {
width: 100%;
display: flex;
margin: 2rem 0;
flex-direction: column;
input {
background-color: var(--nav-color);
}
.box-row {
width: 100%;
display: flex;
margin: 0 2rem;
flex-direction: row;
}
.box-col {
display: flex;
min-width: 300px;
flex-direction: column;
}
.line {
margin-bottom: 15px;
}
#pw-change {
input {
margin-bottom: 10px;
}
}
}
}

View file

@ -7,10 +7,11 @@ export interface AuthData {
} }
interface AuthState { export interface AuthState {
token: string | null; token: string | null;
setToken: (token: string) => void; setToken: (token: string) => void;
clearToken: () => void; clearToken: () => void;
checkToken: (token: string) => Promise<boolean>;
} }
export const useAuthStore = create<AuthState>()( export const useAuthStore = create<AuthState>()(
@ -19,6 +20,15 @@ export const useAuthStore = create<AuthState>()(
token: null, token: null,
setToken: (token) => set({ token }), setToken: (token) => set({ token }),
clearToken: () => set({ token: null }), clearToken: () => set({ token: null }),
checkToken: async (token: string) => {
const res = await fetch("/api/auth/check", {
headers: {
"Authorization": `Basic ${token}`
}
});
return res.status === 200;
}
}), }),
{ {
name: "auth-storage" name: "auth-storage"