feat: add login
This commit is contained in:
parent
5413244e60
commit
295f3ecd14
15 changed files with 559 additions and 13 deletions
45
app.go
45
app.go
|
@ -1,8 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"git.wh64.net/devproje/kuma-archive/config"
|
"git.wh64.net/devproje/kuma-archive/config"
|
||||||
"git.wh64.net/devproje/kuma-archive/internal/routes"
|
"git.wh64.net/devproje/kuma-archive/internal/routes"
|
||||||
|
@ -11,6 +13,7 @@ import (
|
||||||
"github.com/devproje/commando/option"
|
"github.com/devproje/commando/option"
|
||||||
"github.com/devproje/commando/types"
|
"github.com/devproje/commando/types"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -40,6 +43,9 @@ func main() {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// init auth module
|
||||||
|
service.NewAuthService()
|
||||||
|
|
||||||
gin := gin.Default()
|
gin := gin.Default()
|
||||||
routes.New(gin, ver, apiOnly)
|
routes.New(gin, ver, apiOnly)
|
||||||
|
|
||||||
|
@ -65,6 +71,45 @@ func main() {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
command.ComplexRoot("account", "file server account manager", []commando.Node{
|
||||||
|
command.Then("create", "create account", func(n *commando.Node) error {
|
||||||
|
var username, password string
|
||||||
|
|
||||||
|
fmt.Print("new username: ")
|
||||||
|
if _, err := fmt.Scanln(&username); err != nil {
|
||||||
|
return fmt.Errorf("failed to read username: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("new password: ")
|
||||||
|
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read password: %v", err)
|
||||||
|
}
|
||||||
|
password = string(bytePassword)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Print("type new password one more time: ")
|
||||||
|
checkByte, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read password: %v", err)
|
||||||
|
}
|
||||||
|
check := string(checkByte)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if password != check {
|
||||||
|
return errors.New("password check is not correct")
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := service.NewAuthService()
|
||||||
|
if err := auth.Create(&service.Account{Username: username, Password: password}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Account for %s created successfully\n", username)
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
if err := command.Execute(); err != nil {
|
if err := command.Execute(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -33,6 +33,7 @@ require (
|
||||||
golang.org/x/crypto v0.36.0 // indirect
|
golang.org/x/crypto v0.36.0 // indirect
|
||||||
golang.org/x/net v0.37.0 // indirect
|
golang.org/x/net v0.37.0 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
|
golang.org/x/term v0.30.0 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.5 // indirect
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -85,6 +85,8 @@ golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||||
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
|
|
@ -106,6 +106,38 @@ func New(app *gin.Engine, version *service.Version, apiOnly bool) {
|
||||||
ctx.FileAttachment(data.Path, data.Name)
|
ctx.FileAttachment(data.Path, data.Name)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
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())
|
||||||
})
|
})
|
||||||
|
|
168
internal/service/auth.go
Normal file
168
internal/service/auth.go
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthService struct{}
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Salt string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthService() *AuthService {
|
||||||
|
return &AuthService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
db, err := Open()
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
stmt, err := db.Prepare(strings.TrimSpace(`
|
||||||
|
create table Account(
|
||||||
|
username varchar(25),
|
||||||
|
password varchar(255),
|
||||||
|
salt varchar(50),
|
||||||
|
primary key (username)
|
||||||
|
);
|
||||||
|
`))
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
if _, err = stmt.Exec(); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) Create(data *Account) error {
|
||||||
|
db, err := Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
stmt, err := db.Prepare("insert into Account values(?, ?, ?);")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
salt := genSalt()
|
||||||
|
if _, err = stmt.Exec(data.Username, encrypt(data.Password, salt), salt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) Read(username string) (*Account, error) {
|
||||||
|
db, err := Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
stmt, err := db.Prepare("select * from Account where username = ?;")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
var account Account
|
||||||
|
if err := stmt.QueryRow(username).Scan(&account.Username, &account.Password, &account.Salt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) Update(username, password string) error {
|
||||||
|
db, err := Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
stmt, err := db.Prepare("update Account set password = ?, salt = ? where username = ?;")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
salt := genSalt()
|
||||||
|
if _, err = stmt.Exec(encrypt(password, salt), salt, username); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) Delete(username string) error {
|
||||||
|
db, err := Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
stmt, err := db.Prepare("delete from Account where username = ?;")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
if _, err = stmt.Exec(username); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) Verify(username, password string) (bool, error) {
|
||||||
|
account, err := s.Read(username)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if encrypt(password, account.Salt) == 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
func encrypt(password, salt string) string {
|
||||||
|
hash := sha256.New()
|
||||||
|
hash.Write([]byte(password + salt))
|
||||||
|
return hex.EncodeToString(hash.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func genSalt() string {
|
||||||
|
b := make([]byte, 16)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b)
|
||||||
|
}
|
|
@ -2,11 +2,12 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"git.wh64.net/devproje/kuma-archive/config"
|
"git.wh64.net/devproje/kuma-archive/config"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Open() (*sql.DB, error) {
|
func Open() (*sql.DB, error) {
|
||||||
return sql.Open("sqlite3", (config.ROOT_DIRECTORY))
|
return sql.Open("sqlite3", filepath.Join(config.ROOT_DIRECTORY, "data.db"))
|
||||||
}
|
}
|
||||||
|
|
27
src/App.scss
27
src/App.scss
|
@ -31,9 +31,36 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
display: flex;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 0.45rem 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
transition-duration: 300ms;
|
||||||
|
background-color: var(--nav-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transition-duration: 300ms;
|
||||||
|
background-color: var(--nav-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
51
src/App.tsx
51
src/App.tsx
|
@ -9,18 +9,33 @@ import { BrowserRouter, Route, Routes, useLocation } from "react-router";
|
||||||
import "./App.scss";
|
import "./App.scss";
|
||||||
import kuma from "./assets/kuma.png";
|
import kuma from "./assets/kuma.png";
|
||||||
import NotFound from "./components/notfound";
|
import NotFound from "./components/notfound";
|
||||||
|
import Login from "./components/login";
|
||||||
|
import { useAuthStore } from "./store/auth";
|
||||||
|
import Logout from "./components/logout";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={"*"} element={<Dashboard />} />
|
<Route path="/login" element={<Dashboard children={<Login />} />} />
|
||||||
|
<Route path="/logout" element={<Logout />} />
|
||||||
|
<Route path={"*"} element={<Dashboard children={<View />} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Dashboard() {
|
function Dashboard({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<main className="container-md ka-view">
|
||||||
|
<Header />
|
||||||
|
{children}
|
||||||
|
<Footer />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function View() {
|
||||||
const path = usePath();
|
const path = usePath();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [load, setLoad] = useState(false);
|
const [load, setLoad] = useState(false);
|
||||||
|
@ -39,16 +54,20 @@ function Dashboard() {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (typeof path.data === "undefined") {
|
||||||
<main className="container-md ka-view">
|
return <NotFound />;
|
||||||
<Header />
|
}
|
||||||
{typeof path.data !== "undefined" ? path.data.is_dir ? <Directory /> : <FileView /> : <NotFound />}
|
|
||||||
<Footer />
|
if (path.data.is_dir) {
|
||||||
</main>
|
return <Directory />;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
return <FileView />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
|
const auth = useAuthStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="ka-nav">
|
<nav className="ka-nav">
|
||||||
<a className="title" href="/">
|
<a className="title" href="/">
|
||||||
|
@ -63,6 +82,20 @@ function Header() {
|
||||||
<a className="link" href="https://projecttl.net">
|
<a className="link" href="https://projecttl.net">
|
||||||
<DynamicIcon name="globe" size={15} />
|
<DynamicIcon name="globe" size={15} />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
{!auth.token ? (
|
||||||
|
<a className="login-btn" href="/login">
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<div className="login-info">
|
||||||
|
<span>Logged in as Admin</span>
|
||||||
|
<a className="login-btn" href="/logout">
|
||||||
|
Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
#download {
|
#download {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
|
||||||
a {
|
.download-btn {
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
span {
|
span {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
|
|
|
@ -107,7 +107,7 @@ function FileView() {
|
||||||
{convert(path.data.total)}
|
{convert(path.data.total)}
|
||||||
|
|
||||||
<div id="download">
|
<div id="download">
|
||||||
<button className="primary" id="download" onClick={ev => {
|
<button className="download-btn secondary" onClick={ev => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = `/api/download${path.data?.path}`;
|
link.href = `/api/download${path.data?.path}`;
|
||||||
|
|
100
src/components/login/index.tsx
Normal file
100
src/components/login/index.tsx
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
|
import "./login.scss";
|
||||||
|
import project from "../../assets/kuma.png";
|
||||||
|
import { DynamicIcon } from "lucide-react/dynamic";
|
||||||
|
import { AuthData, useAuthStore } from "../../store/auth";
|
||||||
|
|
||||||
|
function Login() {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const auth = useAuthStore();
|
||||||
|
|
||||||
|
const usernameRef = useRef<HTMLInputElement>(null);
|
||||||
|
const passwordRef = useRef<HTMLInputElement>(null);
|
||||||
|
const errnoRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
if (auth.token !== null)
|
||||||
|
document.location.href = "/";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ka-login">
|
||||||
|
<form className="login-form">
|
||||||
|
<div className="logo">
|
||||||
|
<img src={project} />
|
||||||
|
<h1>Kuma Archive Login</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input-area">
|
||||||
|
<div className="input">
|
||||||
|
<div className="input-icon">
|
||||||
|
<DynamicIcon name="user" size={20} />
|
||||||
|
</div>
|
||||||
|
<input ref={usernameRef} placeholder="Username" type="text" required />
|
||||||
|
<div className="dummy"></div>
|
||||||
|
</div>
|
||||||
|
<div className="input">
|
||||||
|
<div className="input-icon">
|
||||||
|
<DynamicIcon name="key-round" size={20} />
|
||||||
|
</div>
|
||||||
|
<input ref={passwordRef} placeholder="Password" type={show ? "text" : "password"} required />
|
||||||
|
<a onClick={ev => {
|
||||||
|
ev.preventDefault();
|
||||||
|
setShow(!show);
|
||||||
|
}}>
|
||||||
|
<DynamicIcon name={show ? "eye-off" : "eye"} size={20} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="submit-area">
|
||||||
|
<span className="errno" ref={errnoRef}></span>
|
||||||
|
|
||||||
|
<button type="submit" className="login-btn success" onClick={ev => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const username = usernameRef.current?.value;
|
||||||
|
const password = passwordRef.current?.value;
|
||||||
|
|
||||||
|
if (!errnoRef)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
alert("username or password is empty!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username === "" || password === "") {
|
||||||
|
alert("username or password is empty!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = new URLSearchParams();
|
||||||
|
form.append("username", username);
|
||||||
|
form.append("password", password);
|
||||||
|
|
||||||
|
errnoRef.current!.innerText = "";
|
||||||
|
|
||||||
|
fetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
body: form
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.status !== 200) {
|
||||||
|
errnoRef.current!.innerText = "username or password is not invalid";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json().then((data: AuthData) => {
|
||||||
|
auth.setToken(data.token);
|
||||||
|
window.location.href = "/";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<DynamicIcon name="log-in" size={20} />
|
||||||
|
<span>Login</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
98
src/components/login/login.scss
Normal file
98
src/components/login/login.scss
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
.ka-login {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
margin: 2rem 0;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
width: 400px;
|
||||||
|
height: 500px;
|
||||||
|
display: flex;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
border-radius: 15px;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: var(--nav-color);
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-area {
|
||||||
|
width: 100%;
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
input {
|
||||||
|
border-radius: 0 25px 25px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-icon {
|
||||||
|
padding: 0 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 25px 0 0 25px;
|
||||||
|
background-color: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, .dummy {
|
||||||
|
display: flex;
|
||||||
|
min-width: 50px;
|
||||||
|
min-height: 40px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 0 25px 25px 0;
|
||||||
|
background-color: var(--background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-area {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.errno {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
src/components/logout/index.tsx
Normal file
11
src/components/logout/index.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { useAuthStore } from "../../store/auth";
|
||||||
|
|
||||||
|
function Logout() {
|
||||||
|
const auth = useAuthStore();
|
||||||
|
auth.clearToken();
|
||||||
|
|
||||||
|
document.location.href = "/";
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Logout;
|
|
@ -7,7 +7,7 @@ function NotFound() {
|
||||||
<DynamicIcon className="icon" name="file-question" size={120} />
|
<DynamicIcon className="icon" name="file-question" size={120} />
|
||||||
<h1>404 Not Found</h1>
|
<h1>404 Not Found</h1>
|
||||||
|
|
||||||
<button className="primary" onClick={ev => {
|
<button className="secondary" onClick={ev => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
document.location.href = "/";
|
document.location.href = "/";
|
||||||
}}>Back to home</button>
|
}}>Back to home</button>
|
||||||
|
|
28
src/store/auth.ts
Normal file
28
src/store/auth.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
export interface AuthData {
|
||||||
|
ok: number;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
token: string | null;
|
||||||
|
setToken: (token: string) => void;
|
||||||
|
clearToken: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
token: null,
|
||||||
|
setToken: (token) => set({ token }),
|
||||||
|
clearToken: () => set({ token: null }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "auth-storage"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
Loading…
Reference in a new issue