mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-07 09:00:47 +00:00
feat: web login page (#1058)
This commit is contained in:
22 files changed
+771
-43
No files matched your search
+3
-3
@@ -26,9 +26,9 @@ func parse() *args {
|
||||
var cliArgs args
|
||||
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
||||
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
||||
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
||||
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd")
|
||||
cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling basic authentication")
|
||||
cliArgs.Username = flag.String("u", "gopeed", "Web Authentication Username")
|
||||
cliArgs.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
|
||||
cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
|
||||
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
|
||||
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
||||
flag.Parse()
|
||||
|
||||
+3
-3
@@ -23,9 +23,9 @@ func main() {
|
||||
}
|
||||
|
||||
args := parse()
|
||||
var webBasicAuth *model.WebBasicAuth
|
||||
var webAuth *model.WebAuth
|
||||
if isNotBlank(args.Username) && isNotBlank(args.Password) {
|
||||
webBasicAuth = &model.WebBasicAuth{
|
||||
webAuth = &model.WebAuth{
|
||||
Username: *args.Username,
|
||||
Password: *args.Password,
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func main() {
|
||||
ProductionMode: true,
|
||||
WebEnable: true,
|
||||
WebFS: sub,
|
||||
WebBasicAuth: webBasicAuth,
|
||||
WebAuth: webAuth,
|
||||
}
|
||||
cmd.Start(cfg)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ require (
|
||||
github.com/go-git/go-git/v5 v5.8.1
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/imroc/req/v3 v3.52.2
|
||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||
github.com/mattn/go-ieproxy v0.0.12
|
||||
github.com/pkg/errors v0.9.1
|
||||
@@ -29,11 +30,9 @@ require (
|
||||
github.com/ebitengine/purego v0.8.1 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/icholy/digest v1.1.0 // indirect
|
||||
github.com/imroc/req/v3 v3.52.2 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||
|
||||
@@ -347,10 +347,11 @@ github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJm
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
@@ -399,6 +400,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI=
|
||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
@@ -497,6 +500,7 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad h1:QtuWk6dsrNXc/ugwCBEN0jG52q/4tXRzMvZm4fH6T9g=
|
||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad/go.mod h1:Jj8p9bgKGTPQ+M8CdUMS9p7Mmdoxa3OAcAjJQBu0CcI=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
@@ -675,6 +679,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -696,6 +702,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||
|
||||
@@ -309,6 +309,8 @@ func DoProxy(w http.ResponseWriter, r *http.Request) {
|
||||
r.RequestURI = ""
|
||||
r.URL = targetUrl
|
||||
r.Host = targetUrl.Host
|
||||
r.Header.Del("Authorization")
|
||||
r.Header.Del("X-Target-Uri")
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
writeError(w, err.Error())
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"io/fs"
|
||||
)
|
||||
@@ -25,9 +24,9 @@ type StartConfig struct {
|
||||
|
||||
ProductionMode bool
|
||||
|
||||
WebEnable bool
|
||||
WebFS fs.FS
|
||||
WebBasicAuth *WebBasicAuth
|
||||
WebEnable bool
|
||||
WebFS fs.FS
|
||||
WebAuth *WebAuth
|
||||
}
|
||||
|
||||
func (cfg *StartConfig) Init() *StartConfig {
|
||||
@@ -49,13 +48,7 @@ func (cfg *StartConfig) Init() *StartConfig {
|
||||
return cfg
|
||||
}
|
||||
|
||||
type WebBasicAuth struct {
|
||||
type WebAuth struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// Authorization returns the value of the Authorization header to be used in HTTP requests.
|
||||
func (cfg *WebBasicAuth) Authorization() string {
|
||||
userId := cfg.Username + ":" + cfg.Password
|
||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(userId))
|
||||
}
|
||||
+123
-9
@@ -2,6 +2,10 @@ package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
@@ -10,12 +14,14 @@ import (
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -23,6 +29,7 @@ import (
|
||||
var (
|
||||
srv *http.Server
|
||||
runningPort int
|
||||
aesKey []byte
|
||||
|
||||
Downloader *download.Downloader
|
||||
)
|
||||
@@ -95,6 +102,13 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
||||
util.SafeRemove(startCfg.Address)
|
||||
}
|
||||
|
||||
if startCfg.WebEnable {
|
||||
aesKey = make([]byte, 32)
|
||||
if _, err := rand.Read(aesKey); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "generate aes key failed")
|
||||
}
|
||||
}
|
||||
|
||||
listener, err := net.Listen(startCfg.Network, startCfg.Address)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -125,15 +139,40 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
||||
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension)
|
||||
r.Path("/api/v1/proxy").HandlerFunc(DoProxy)
|
||||
|
||||
enableApiToken := startCfg.ApiToken != ""
|
||||
enableBasicAuth := startCfg.WebEnable && startCfg.WebAuth != nil
|
||||
if startCfg.WebEnable {
|
||||
if enableBasicAuth {
|
||||
r.Methods(http.MethodPost).Path("/api/web/login").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var loginReq model.WebAuth
|
||||
if ReadJson(r, w, &loginReq) {
|
||||
if loginReq.Username == startCfg.WebAuth.Username && loginReq.Password == startCfg.WebAuth.Password {
|
||||
// Generate a login token, Username:Password:Timestamp
|
||||
timestamp := time.Now().Unix()
|
||||
tokenData := fmt.Sprintf("%s:%s:%d", loginReq.Username, loginReq.Password, timestamp)
|
||||
token, err := aesEncrypt(aesKey, []byte(tokenData))
|
||||
if err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
WriteJson(w, model.NewOkResult(token))
|
||||
return
|
||||
}
|
||||
}
|
||||
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
||||
})
|
||||
}
|
||||
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
|
||||
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
|
||||
r.PathPrefix("/").Handler(gzipMiddleware(http.FileServer(newEmbedCacheFileSystem(http.FS(startCfg.WebFS)))))
|
||||
}
|
||||
|
||||
enableApiToken := startCfg.ApiToken != ""
|
||||
enableBasicAuth := startCfg.WebEnable && startCfg.WebBasicAuth != nil
|
||||
if enableApiToken || enableBasicAuth {
|
||||
writeUnauthorized := func(w http.ResponseWriter, r *http.Request) {
|
||||
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
||||
}
|
||||
|
||||
r.Use(func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if enableApiToken {
|
||||
@@ -145,19 +184,49 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
||||
return
|
||||
}
|
||||
|
||||
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
||||
writeUnauthorized(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if startCfg.WebEnable && startCfg.WebBasicAuth != nil {
|
||||
if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() {
|
||||
if enableBasicAuth {
|
||||
if r.URL.Path == "/api/web/login" {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"gopeed web\"")
|
||||
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
writeUnauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
tokenData, err := aesDecrypt(aesKey, token)
|
||||
if err != nil {
|
||||
writeUnauthorized(w, r)
|
||||
return
|
||||
}
|
||||
parts := strings.SplitN(string(tokenData), ":", 3)
|
||||
username := parts[0]
|
||||
password := parts[1]
|
||||
timestamp, _ := strconv.Atoi(parts[2])
|
||||
|
||||
if username != startCfg.WebAuth.Username || password != startCfg.WebAuth.Password {
|
||||
writeUnauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the token is expired (7 days)
|
||||
if time.Now().Unix()-int64(timestamp) > 7*24*3600 {
|
||||
writeUnauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
||||
writeUnauthorized(w, r)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -177,9 +246,10 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
||||
})
|
||||
|
||||
srv = &http.Server{Handler: handlers.CORS(
|
||||
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}),
|
||||
handlers.AllowedHeaders([]string{"Content-Type", "Authorization", "X-Api-Token", "X-Target-Uri"}),
|
||||
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
|
||||
handlers.AllowedOrigins([]string{"*"}),
|
||||
handlers.AllowCredentials(),
|
||||
)(r)}
|
||||
return srv, listener, nil
|
||||
}
|
||||
@@ -315,3 +385,47 @@ func WriteStatusJson(w http.ResponseWriter, statusCode int, v any) {
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func aesEncrypt(key, data []byte) (string, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cipherText := gcm.Seal(nonce, nonce, data, nil)
|
||||
return base64.StdEncoding.EncodeToString(cipherText), nil
|
||||
}
|
||||
|
||||
func aesDecrypt(key []byte, encryptedData string) ([]byte, error) {
|
||||
cipherText, err := base64.StdEncoding.DecodeString(encryptedData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(cipherText) < gcm.NonceSize() {
|
||||
return nil, errors.New("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, cipherText := cipherText[:gcm.NonceSize()], cipherText[gcm.NonceSize():]
|
||||
return gcm.Open(nil, nonce, cipherText, nil)
|
||||
}
|
||||
+52
-5
@@ -637,7 +637,7 @@ func TestAuthorization(t *testing.T) {
|
||||
cfg.Init()
|
||||
cfg.ApiToken = "123456"
|
||||
cfg.WebEnable = true
|
||||
cfg.WebBasicAuth = &model.WebBasicAuth{
|
||||
cfg.WebAuth = &model.WebAuth{
|
||||
Username: "admin",
|
||||
Password: "123456",
|
||||
}
|
||||
@@ -649,14 +649,61 @@ func TestAuthorization(t *testing.T) {
|
||||
Stop()
|
||||
}()
|
||||
|
||||
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
||||
status, _ := doHttpRequest0(http.MethodPost, "/api/web/login", nil, &model.WebAuth{
|
||||
Username: "xxx",
|
||||
Password: "xxx",
|
||||
})
|
||||
if status != http.StatusUnauthorized {
|
||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
token := httpRequestCheckOk[string](http.MethodPost, "/api/web/login", cfg.WebAuth)
|
||||
authToken := fmt.Sprintf("Bearer %s", token)
|
||||
authHeaders := map[string]string{
|
||||
"Authorization": authToken,
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
||||
if status != http.StatusUnauthorized {
|
||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"Authorization": cfg.WebBasicAuth.Authorization(),
|
||||
"Authorization": "xxx",
|
||||
}, nil)
|
||||
if status != http.StatusUnauthorized {
|
||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"Authorization": "xxx",
|
||||
}, nil)
|
||||
if status != http.StatusUnauthorized {
|
||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
buildToken := func(username, password string, ts int64) string {
|
||||
token, _ := aesEncrypt(aesKey, []byte(fmt.Sprintf("%s:%s:%d", username, password, ts)))
|
||||
return token
|
||||
}
|
||||
|
||||
fakeToken := buildToken("fake", "fake", time.Now().Unix())
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", fakeToken),
|
||||
}, nil)
|
||||
if status != http.StatusUnauthorized {
|
||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
expireToken := buildToken(cfg.WebAuth.Username, cfg.WebAuth.Password, time.Now().Add(-time.Hour*8*24).Unix())
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", expireToken),
|
||||
}, nil)
|
||||
if status != http.StatusUnauthorized {
|
||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", authHeaders, nil)
|
||||
if status != http.StatusOK {
|
||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
||||
}
|
||||
@@ -669,7 +716,7 @@ func TestAuthorization(t *testing.T) {
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"Authorization": cfg.WebBasicAuth.Authorization(),
|
||||
"Authorization": authToken,
|
||||
"X-Api-Token": cfg.ApiToken,
|
||||
}, nil)
|
||||
if status != http.StatusOK {
|
||||
@@ -677,7 +724,7 @@ func TestAuthorization(t *testing.T) {
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"Authorization": cfg.WebBasicAuth.Authorization(),
|
||||
"Authorization": authToken,
|
||||
"X-Api-Token": "",
|
||||
}, nil)
|
||||
if status != http.StatusUnauthorized {
|
||||
|
||||
@@ -3,13 +3,17 @@ import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart' as getx;
|
||||
|
||||
import '../app/routes/app_pages.dart';
|
||||
import '../database/database.dart';
|
||||
import '../util/util.dart';
|
||||
import 'model/create_task.dart';
|
||||
import 'model/create_task_batch.dart';
|
||||
import 'model/downloader_config.dart';
|
||||
import 'model/extension.dart';
|
||||
import 'model/install_extension.dart';
|
||||
import 'model/login.dart';
|
||||
import 'model/request.dart';
|
||||
import 'model/resolve_result.dart';
|
||||
import 'model/result.dart';
|
||||
@@ -43,12 +47,27 @@ class _Client {
|
||||
dio.options.sendTimeout = const Duration(seconds: 5);
|
||||
dio.options.connectTimeout = const Duration(seconds: 5);
|
||||
dio.options.receiveTimeout = const Duration(seconds: 60);
|
||||
dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
|
||||
if (apiToken.isNotEmpty) {
|
||||
options.headers['X-Api-Token'] = apiToken;
|
||||
}
|
||||
handler.next(options);
|
||||
}));
|
||||
dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
if (apiToken.isNotEmpty) {
|
||||
options.headers['X-Api-Token'] = apiToken;
|
||||
}
|
||||
if (Util.isWeb()) {
|
||||
final token = Database.instance.getWebToken();
|
||||
if (token != null) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
}
|
||||
handler.next(options);
|
||||
},
|
||||
onError: (error, handler) {
|
||||
// Only web version has a login page
|
||||
if (Util.isWeb() && error.response?.statusCode == 401) {
|
||||
getx.Get.rootDelegate.offAndToNamed(Routes.LOGIN);
|
||||
}
|
||||
handler.next(error);
|
||||
},
|
||||
));
|
||||
|
||||
_instance!.dio = dio;
|
||||
if (isUnixSocket) {
|
||||
@@ -96,7 +115,8 @@ Future<T> _parse<T>(
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.sendTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout ||
|
||||
e.type == DioExceptionType.connectionTimeout) {
|
||||
e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.connectionError) {
|
||||
throw TimeoutException("request timeout");
|
||||
}
|
||||
throw Exception(Result(code: 1000, msg: e.message));
|
||||
@@ -216,6 +236,11 @@ Future<void> updateExtension(String identity) async {
|
||||
() => _client.dio.post("api/v1/extensions/$identity/update"), null);
|
||||
}
|
||||
|
||||
Future<String> login(LoginReq loginReq) async {
|
||||
return _parse(() => _client.dio.post("api/web/login", data: loginReq),
|
||||
(data) => data as String);
|
||||
}
|
||||
|
||||
Future<Response<String>> proxyRequest<T>(String uri,
|
||||
{data, Options? options}) async {
|
||||
options ??= Options();
|
||||
@@ -230,5 +255,9 @@ Future<Response<String>> proxyRequest<T>(String uri,
|
||||
}
|
||||
|
||||
String join(String path) {
|
||||
return "${_client.dio.options.baseUrl}/${Util.cleanPath(path)}";
|
||||
final baseUrl = _client.dio.options.baseUrl;
|
||||
final cleanBaseUrl = baseUrl.endsWith('/')
|
||||
? baseUrl.substring(0, baseUrl.length - 1)
|
||||
: baseUrl;
|
||||
return "$cleanBaseUrl/${Util.cleanPath(path)}";
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'login.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class LoginReq {
|
||||
String username;
|
||||
String password;
|
||||
|
||||
LoginReq({
|
||||
required this.username,
|
||||
required this.password,
|
||||
});
|
||||
|
||||
factory LoginReq.fromJson(Map<String, dynamic> json) =>
|
||||
_$LoginReqFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$LoginReqToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'login.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
LoginReq _$LoginReqFromJson(Map<String, dynamic> json) => LoginReq(
|
||||
username: json['username'] as String,
|
||||
password: json['password'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LoginReqToJson(LoginReq instance) => <String, dynamic>{
|
||||
'username': instance.username,
|
||||
'password': instance.password,
|
||||
};
|
||||
@@ -16,6 +16,7 @@ import '../../../../api/model/extension.dart';
|
||||
import '../../../../api/model/install_extension.dart';
|
||||
import '../../../../api/model/switch_extension.dart';
|
||||
import '../../../../api/model/update_extension_settings.dart';
|
||||
import '../../../../database/database.dart';
|
||||
import '../../../../util/message.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../views/icon_button_loading.dart';
|
||||
@@ -135,6 +136,10 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
'/fs/extensions/${extension.identity}/${extension.icon}'),
|
||||
width: 48,
|
||||
height: 48,
|
||||
headers: {
|
||||
'Authorization':
|
||||
'Bearer ${Database.instance.getWebToken()}'
|
||||
},
|
||||
)
|
||||
: Image.file(
|
||||
extension.devMode
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/login_controller.dart';
|
||||
|
||||
class LoginBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<LoginController>(
|
||||
() => LoginController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../../api/api.dart' as api;
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/model/login.dart';
|
||||
import '../../../../database/database.dart';
|
||||
import '../../../../util/message.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import '../../app/controllers/app_controller.dart';
|
||||
|
||||
class LoginController extends GetxController {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final usernameController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
|
||||
final isLoading = false.obs;
|
||||
final passwordVisible = false.obs;
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
usernameController.dispose();
|
||||
passwordController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void togglePasswordVisibility() {
|
||||
passwordVisible.value = !passwordVisible.value;
|
||||
}
|
||||
|
||||
Future<void> login() async {
|
||||
if (!formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
final loginReq = LoginReq(
|
||||
username: usernameController.text.trim(),
|
||||
password: passwordController.text,
|
||||
);
|
||||
|
||||
final token = await api.login(loginReq);
|
||||
// Login successful, save the token
|
||||
Database.instance.saveWebToken(token);
|
||||
// Reload config
|
||||
final controller = Get.put(AppController());
|
||||
await controller.loadDownloaderConfig();
|
||||
// Navigate to home page
|
||||
Get.rootDelegate.offAndToNamed(Routes.HOME);
|
||||
} catch (e) {
|
||||
if (e is TimeoutException) {
|
||||
showMessage('error'.tr, 'login_failed_network'.tr);
|
||||
} else {
|
||||
showMessage('error'.tr, 'login_failed'.tr);
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../views/responsive_builder.dart';
|
||||
import '../controllers/login_controller.dart';
|
||||
|
||||
class LoginView extends GetView<LoginController> {
|
||||
const LoginView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isNarrow = ResponsiveBuilder.isNarrow(context);
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Get.theme.colorScheme.surface,
|
||||
Get.theme.colorScheme.surface.withOpacity(0.8),
|
||||
Get.theme.colorScheme.surface.withOpacity(0.9),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isNarrow ? 16.0 : 32.0,
|
||||
vertical: isNarrow ? 24.0 : 32.0,
|
||||
),
|
||||
child: Card(
|
||||
elevation: 12,
|
||||
shadowColor: Get.theme.colorScheme.shadow.withOpacity(0.3),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(isNarrow ? 20 : 24),
|
||||
),
|
||||
color: Get.theme.colorScheme.surface,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: isNarrow ? double.infinity : 420,
|
||||
),
|
||||
padding: EdgeInsets.all(isNarrow ? 24.0 : 40.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(isNarrow ? 20 : 24),
|
||||
border: Border.all(
|
||||
color: Get.theme.colorScheme.outline.withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: FocusTraversalGroup(
|
||||
policy: OrderedTraversalPolicy(),
|
||||
child: Form(
|
||||
key: controller.formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Logo/Title Section
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
margin: EdgeInsets.only(bottom: isNarrow ? 32 : 48),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(isNarrow ? 8 : 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(isNarrow ? 20 : 24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Get.theme.colorScheme.primary
|
||||
.withOpacity(0.2),
|
||||
blurRadius: isNarrow ? 16 : 24,
|
||||
offset: Offset(0, isNarrow ? 8 : 12),
|
||||
spreadRadius: isNarrow ? 1 : 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.circular(isNarrow ? 16 : 20),
|
||||
child: SvgPicture.asset(
|
||||
'assets/icon/icon.svg',
|
||||
width: isNarrow ? 56 : 72,
|
||||
height: isNarrow ? 56 : 72,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Gopeed',
|
||||
style: Get.textTheme.headlineLarge?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Get.theme.colorScheme.onSurface,
|
||||
letterSpacing: 2.0,
|
||||
fontSize: isNarrow ? 28 : 36,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Username Field
|
||||
FocusTraversalOrder(
|
||||
order: const NumericFocusOrder(1.0),
|
||||
child: TextFormField(
|
||||
controller: controller.usernameController,
|
||||
autofillHints: const [
|
||||
AutofillHints.username,
|
||||
],
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.done,
|
||||
onFieldSubmitted: (_) => controller.login(),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'username'.tr,
|
||||
labelStyle: TextStyle(
|
||||
color: Get.theme.colorScheme.onSurface
|
||||
.withOpacity(0.7),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
prefixIcon: Container(
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Get.theme.colorScheme.primary
|
||||
.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.person_outline_rounded,
|
||||
color: Get.theme.colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide(
|
||||
color: Get.theme.colorScheme.outline
|
||||
.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide(
|
||||
color: Get.theme.colorScheme.outline
|
||||
.withOpacity(0.3),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide(
|
||||
color: Get.theme.colorScheme.primary,
|
||||
width: 2.5,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide(
|
||||
color: Get.theme.colorScheme.error,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide(
|
||||
color: Get.theme.colorScheme.error,
|
||||
width: 2.5,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Get
|
||||
.theme.colorScheme.surfaceContainerHighest
|
||||
.withOpacity(0.8),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 20,
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'username_required'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: isNarrow ? 16 : 24),
|
||||
|
||||
// Password Field
|
||||
FocusTraversalOrder(
|
||||
order: const NumericFocusOrder(2.0),
|
||||
child: Obx(() => TextFormField(
|
||||
controller: controller.passwordController,
|
||||
autofillHints: const [
|
||||
AutofillHints.password,
|
||||
],
|
||||
obscureText: !controller.passwordVisible.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'password'.tr,
|
||||
labelStyle: TextStyle(
|
||||
color: Get.theme.colorScheme.onSurface
|
||||
.withOpacity(0.7),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
prefixIcon: Container(
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Get.theme.colorScheme.primary
|
||||
.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.lock_outline_rounded,
|
||||
color: Get.theme.colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
controller.passwordVisible.value
|
||||
? Icons.visibility_off_rounded
|
||||
: Icons.visibility_rounded,
|
||||
color: Get.theme.colorScheme.onSurface
|
||||
.withOpacity(0.6),
|
||||
),
|
||||
onPressed:
|
||||
controller.togglePasswordVisibility,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide(
|
||||
color: Get.theme.colorScheme.outline
|
||||
.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide(
|
||||
color: Get.theme.colorScheme.outline
|
||||
.withOpacity(0.3),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide(
|
||||
color: Get.theme.colorScheme.primary,
|
||||
width: 2.5,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide(
|
||||
color: Get.theme.colorScheme.error,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide(
|
||||
color: Get.theme.colorScheme.error,
|
||||
width: 2.5,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Get
|
||||
.theme.colorScheme.surfaceContainerHighest
|
||||
.withOpacity(0.8),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 20,
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'password_required'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
textInputAction: TextInputAction.done,
|
||||
onFieldSubmitted: (_) => controller.login(),
|
||||
)),
|
||||
),
|
||||
|
||||
SizedBox(height: isNarrow ? 24 : 32),
|
||||
|
||||
// Login Button
|
||||
Obx(() => ElevatedButton(
|
||||
onPressed: controller.isLoading.value
|
||||
? null
|
||||
: controller.login,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: isNarrow ? 16 : 18,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
backgroundColor: controller.isLoading.value
|
||||
? Get.theme.disabledColor
|
||||
: Get.theme.colorScheme.primary,
|
||||
foregroundColor:
|
||||
Get.theme.colorScheme.onPrimary,
|
||||
elevation: controller.isLoading.value ? 0 : 4,
|
||||
shadowColor: Get.theme.colorScheme.primary
|
||||
.withOpacity(0.4),
|
||||
),
|
||||
child: controller.isLoading.value
|
||||
? SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.5,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(Get
|
||||
.theme.colorScheme.onPrimary),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'login'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import '../modules/extension/bindings/extension_binding.dart';
|
||||
import '../modules/extension/views/extension_view.dart';
|
||||
import '../modules/home/bindings/home_binding.dart';
|
||||
import '../modules/home/views/home_view.dart';
|
||||
import '../modules/login/bindings/login_binding.dart';
|
||||
import '../modules/login/views/login_view.dart';
|
||||
import '../modules/redirect/bindings/redirect_binding.dart';
|
||||
import '../modules/redirect/views/redirect_view.dart';
|
||||
import '../modules/root/bindings/root_binding.dart';
|
||||
@@ -63,6 +65,12 @@ class AppPages {
|
||||
binding: SettingBinding(),
|
||||
),
|
||||
]),
|
||||
GetPage(
|
||||
name: _Paths.LOGIN,
|
||||
page: () => const LoginView(),
|
||||
binding: LoginBinding(),
|
||||
transition: Transition.fadeIn,
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.CREATE,
|
||||
transition: Transition.downToUp,
|
||||
|
||||
@@ -7,6 +7,7 @@ abstract class Routes {
|
||||
static const ROOT = _Paths.ROOT;
|
||||
static const HOME = _Paths.HOME;
|
||||
static const CREATE = _Paths.CREATE;
|
||||
static const LOGIN = _Paths.LOGIN;
|
||||
static const TASK = _Paths.HOME + _Paths.TASK;
|
||||
static const TASK_FILES = TASK + _Paths.TASK_FILES;
|
||||
static const EXTENSION = _Paths.HOME + _Paths.EXTENSION;
|
||||
@@ -20,6 +21,7 @@ abstract class _Paths {
|
||||
static const ROOT = '/';
|
||||
static const HOME = '/home';
|
||||
static const CREATE = '/create';
|
||||
static const LOGIN = '/login';
|
||||
static const TASK = '/task';
|
||||
static const TASK_FILES = '/files';
|
||||
static const EXTENSION = '/extension';
|
||||
|
||||
@@ -6,10 +6,10 @@ import 'package:hive/hive.dart';
|
||||
import 'entity.dart';
|
||||
|
||||
const String _startConfig = 'startConfig';
|
||||
const String _lastRunningConfig = 'lastRunningConfig';
|
||||
const String _windowState = 'windowState';
|
||||
const String _bookmark = 'bookmark';
|
||||
const String _createHistory = 'createHistory';
|
||||
const String _webToken = 'webToken';
|
||||
|
||||
class Database {
|
||||
static final Database _instance = Database._internal();
|
||||
@@ -82,6 +82,14 @@ class Database {
|
||||
});
|
||||
}
|
||||
|
||||
void saveWebToken(String token) {
|
||||
save<String>(_webToken, token);
|
||||
}
|
||||
|
||||
String? getWebToken() {
|
||||
return get<String>(_webToken, (json) => json.toString());
|
||||
}
|
||||
|
||||
void saveCreateHistory(String url) {
|
||||
var list = getCreateHistory() ?? [];
|
||||
list.remove(url);
|
||||
|
||||
@@ -118,5 +118,12 @@ const enUS = {
|
||||
'fileSelectedSize': 'Size: ',
|
||||
'httpHeaderName': 'Header Name',
|
||||
'httpHeaderValue': 'Header Value',
|
||||
'login': 'Login',
|
||||
'username_required': 'Please enter your username',
|
||||
'password_required': 'Please enter your password',
|
||||
'login_success': 'Login successful',
|
||||
'login_failed': 'Login failed, please check your username and password',
|
||||
'login_failed_network':
|
||||
'Login failed, please check your network connection',
|
||||
},
|
||||
};
|
||||
@@ -116,5 +116,11 @@ const zhCN = {
|
||||
'fileSelectedSize': '大小:',
|
||||
'httpHeaderName': '请求头名称',
|
||||
'httpHeaderValue': '请求头值',
|
||||
'login': '登录',
|
||||
'username_required': '请输入用户名',
|
||||
'password_required': '请输入密码',
|
||||
'login_success': '登录成功',
|
||||
'login_failed': '登录失败,请检查用户名和密码',
|
||||
'login_failed_network': '登录失败,请检查网络连接',
|
||||
}
|
||||
};
|
||||
@@ -448,6 +448,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.23"
|
||||
flutter_svg:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.17"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -803,6 +811,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1329,6 +1345,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.1"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.18"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_codec
|
||||
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.13"
|
||||
vector_graphics_compiler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.16"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -77,6 +77,7 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/hui-z/flutter_install_plugin.git
|
||||
ref: cf08af829f4a4145634f8a047108f505fdbe5eaa
|
||||
flutter_svg: ^2.0.17
|
||||
dependency_overrides:
|
||||
permission_handler_windows:
|
||||
git:
|
||||
@@ -113,6 +114,7 @@ flutter:
|
||||
assets:
|
||||
- assets/tray_icon/
|
||||
- assets/extension/
|
||||
- assets/icon/
|
||||
# Browser extension native host binary, see https://github.com/GopeedLab/gopeed/tree/main/cmd/host
|
||||
- assets/exec/
|
||||
|
||||
|
||||
Reference in new issue
Block a user