From 295f3ecd1491539c958ec3d4b6e202560c564ebc Mon Sep 17 00:00:00 2001 From: Project_IO Date: Tue, 18 Mar 2025 16:20:49 +0900 Subject: [PATCH] feat: add login --- app.go | 45 ++++++++ go.mod | 1 + go.sum | 2 + internal/routes/mod.go | 32 ++++++ internal/service/auth.go | 168 ++++++++++++++++++++++++++++ internal/service/database.go | 3 +- src/App.scss | 27 +++++ src/App.tsx | 51 +++++++-- src/components/file-view/fview.scss | 2 +- src/components/file-view/index.tsx | 2 +- src/components/login/index.tsx | 100 +++++++++++++++++ src/components/login/login.scss | 98 ++++++++++++++++ src/components/logout/index.tsx | 11 ++ src/components/notfound/index.tsx | 2 +- src/store/auth.ts | 28 +++++ 15 files changed, 559 insertions(+), 13 deletions(-) create mode 100644 internal/service/auth.go create mode 100644 src/components/login/index.tsx create mode 100644 src/components/login/login.scss create mode 100644 src/components/logout/index.tsx create mode 100644 src/store/auth.ts diff --git a/app.go b/app.go index 1000fdc..52d9581 100644 --- a/app.go +++ b/app.go @@ -1,8 +1,10 @@ package main import ( + "errors" "fmt" "os" + "syscall" "git.wh64.net/devproje/kuma-archive/config" "git.wh64.net/devproje/kuma-archive/internal/routes" @@ -11,6 +13,7 @@ import ( "github.com/devproje/commando/option" "github.com/devproje/commando/types" "github.com/gin-gonic/gin" + "golang.org/x/term" ) var ( @@ -40,6 +43,9 @@ func main() { gin.SetMode(gin.ReleaseMode) } + // init auth module + service.NewAuthService() + gin := gin.Default() routes.New(gin, ver, apiOnly) @@ -65,6 +71,45 @@ func main() { 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 { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) diff --git a/go.mod b/go.mod index d6742ea..2ea1d02 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( golang.org/x/crypto v0.36.0 // indirect golang.org/x/net v0.37.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 google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 6256d51..10109e1 100644 --- a/go.sum +++ b/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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 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/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= diff --git a/internal/routes/mod.go b/internal/routes/mod.go index f9531d4..75aa8d1 100644 --- a/internal/routes/mod.go +++ b/internal/routes/mod.go @@ -106,6 +106,38 @@ func New(app *gin.Engine, version *service.Version, apiOnly bool) { 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) { ctx.String(200, "%s", version.String()) }) diff --git a/internal/service/auth.go b/internal/service/auth.go new file mode 100644 index 0000000..f53af92 --- /dev/null +++ b/internal/service/auth.go @@ -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) +} diff --git a/internal/service/database.go b/internal/service/database.go index 482c78d..35bc4f2 100644 --- a/internal/service/database.go +++ b/internal/service/database.go @@ -2,11 +2,12 @@ package service import ( "database/sql" + "path/filepath" "git.wh64.net/devproje/kuma-archive/config" _ "github.com/mattn/go-sqlite3" ) func Open() (*sql.DB, error) { - return sql.Open("sqlite3", (config.ROOT_DIRECTORY)) + return sql.Open("sqlite3", filepath.Join(config.ROOT_DIRECTORY, "data.db")) } diff --git a/src/App.scss b/src/App.scss index a68a4b2..487d400 100644 --- a/src/App.scss +++ b/src/App.scss @@ -31,9 +31,36 @@ align-items: center; flex-direction: row; justify-content: center; + .link { 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); + } + } } } diff --git a/src/App.tsx b/src/App.tsx index 9fe5db5..036bddf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,18 +9,33 @@ import { BrowserRouter, Route, Routes, useLocation } from "react-router"; import "./App.scss"; import kuma from "./assets/kuma.png"; import NotFound from "./components/notfound"; +import Login from "./components/login"; +import { useAuthStore } from "./store/auth"; +import Logout from "./components/logout"; function App() { return ( - } /> + } />} /> + } /> + } />} /> ); } -function Dashboard() { +function Dashboard({ children }: { children: React.ReactNode }) { + return ( +
+
+ {children} +
+
+ ); +} + +function View() { const path = usePath(); const location = useLocation(); const [load, setLoad] = useState(false); @@ -39,16 +54,20 @@ function Dashboard() { return <>; } - return ( -
-
- {typeof path.data !== "undefined" ? path.data.is_dir ? : : } -
-
- ); + if (typeof path.data === "undefined") { + return ; + } + + if (path.data.is_dir) { + return ; + } + + return ; } function Header() { + const auth = useAuthStore(); + return ( ); diff --git a/src/components/file-view/fview.scss b/src/components/file-view/fview.scss index 1964101..006279f 100644 --- a/src/components/file-view/fview.scss +++ b/src/components/file-view/fview.scss @@ -27,7 +27,7 @@ #download { margin: 1rem 0; - a { + .download-btn { margin: 0 5px; span { margin-left: 5px; diff --git a/src/components/file-view/index.tsx b/src/components/file-view/index.tsx index 72005d2..802fb36 100644 --- a/src/components/file-view/index.tsx +++ b/src/components/file-view/index.tsx @@ -107,7 +107,7 @@ function FileView() { {convert(path.data.total)}
- +
+ + + ); +} + +export default Login; diff --git a/src/components/login/login.scss b/src/components/login/login.scss new file mode 100644 index 0000000..3ae4d66 --- /dev/null +++ b/src/components/login/login.scss @@ -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; + } + } + } + } +} diff --git a/src/components/logout/index.tsx b/src/components/logout/index.tsx new file mode 100644 index 0000000..f1563de --- /dev/null +++ b/src/components/logout/index.tsx @@ -0,0 +1,11 @@ +import { useAuthStore } from "../../store/auth"; + +function Logout() { + const auth = useAuthStore(); + auth.clearToken(); + + document.location.href = "/"; + return <>; +} + +export default Logout; diff --git a/src/components/notfound/index.tsx b/src/components/notfound/index.tsx index d67df30..f3b2511 100644 --- a/src/components/notfound/index.tsx +++ b/src/components/notfound/index.tsx @@ -7,7 +7,7 @@ function NotFound() {

404 Not Found

- diff --git a/src/store/auth.ts b/src/store/auth.ts new file mode 100644 index 0000000..f5753e2 --- /dev/null +++ b/src/store/auth.ts @@ -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()( + persist( + (set) => ({ + token: null, + setToken: (token) => set({ token }), + clearToken: () => set({ token: null }), + }), + { + name: "auth-storage" + } + ) +); +