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 := 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")
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
21
src/App.tsx
21
src/App.tsx
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
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;
|
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"
|
||||||
|
|
Loading…
Reference in a new issue