feat: add dashboard
This commit is contained in:
parent
7c487f6f1b
commit
ad8acdbf15
8 changed files with 296 additions and 40 deletions
88
internal/routes/auth.go
Normal file
88
internal/routes/auth.go
Normal 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)
|
||||
})
|
||||
}
|
|
@ -89,36 +89,7 @@ func New(app *gin.Engine, version *service.Version, apiOnly bool) {
|
|||
})
|
||||
|
||||
auth := api.Group("/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),
|
||||
})
|
||||
})
|
||||
}
|
||||
authentication(auth)
|
||||
|
||||
api.GET("/version", func(ctx *gin.Context) {
|
||||
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) {
|
||||
ctx.File("/web/assets/favicon.ico")
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
@ -146,6 +146,19 @@ func (s *AuthService) Verify(username, password string) (bool, error) {
|
|||
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 {
|
||||
raw := fmt.Sprintf("%s:%s", username, password)
|
||||
return base64.StdEncoding.EncodeToString([]byte(raw))
|
||||
|
|
21
src/App.tsx
21
src/App.tsx
|
@ -12,6 +12,7 @@ import NotFound from "./components/notfound";
|
|||
import Login from "./components/login";
|
||||
import { useAuthStore } from "./store/auth";
|
||||
import Logout from "./components/logout";
|
||||
import Settings from "./components/settings";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
|
@ -19,6 +20,7 @@ function App() {
|
|||
<Routes>
|
||||
<Route path="/login" element={<Dashboard children={<Login />} />} />
|
||||
<Route path="/logout" element={<Logout />} />
|
||||
<Route path="/settings" element={<Dashboard children={<Settings />} />} />
|
||||
<Route path={"*"} element={<Dashboard children={<View />} />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
@ -67,6 +69,18 @@ function View() {
|
|||
|
||||
function Header() {
|
||||
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 (
|
||||
<nav className="ka-nav">
|
||||
|
@ -83,17 +97,22 @@ function Header() {
|
|||
<DynamicIcon name="globe" size={15} />
|
||||
</a>
|
||||
|
||||
{!auth.token ? (
|
||||
{!isAuth ? (
|
||||
<a className="login-btn" href="/login">
|
||||
Login
|
||||
</a>
|
||||
) : (
|
||||
<>
|
||||
<a className="link" href="/settings">
|
||||
<DynamicIcon name="settings" size={15} />
|
||||
</a>
|
||||
<div className="login-info">
|
||||
<span>Logged in as Admin</span>
|
||||
<a className="login-btn" href="/logout">
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
|
|
@ -13,8 +13,14 @@ function Login() {
|
|||
const passwordRef = 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 = "/";
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ka-login">
|
||||
|
|
109
src/components/settings/index.tsx
Normal file
109
src/components/settings/index.tsx
Normal 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;
|
41
src/components/settings/settings.scss
Normal file
41
src/components/settings/settings.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,10 +7,11 @@ export interface AuthData {
|
|||
}
|
||||
|
||||
|
||||
interface AuthState {
|
||||
export interface AuthState {
|
||||
token: string | null;
|
||||
setToken: (token: string) => void;
|
||||
clearToken: () => void;
|
||||
checkToken: (token: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
|
@ -19,6 +20,15 @@ export const useAuthStore = create<AuthState>()(
|
|||
token: null,
|
||||
setToken: (token) => set({ token }),
|
||||
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"
|
||||
|
|
Loading…
Reference in a new issue