浏览代码

Update 2.017

+ Added account switching and sub-accounts feature (mobile interface account switching wip)
+ Fixed SMART no disk found bug
+ Fixed desktop css issue
+ Changed wallpapers for some desktop background packs
Toby Chui 1 年之前
父节点
当前提交
c1dbcdfb53

+ 2 - 2
src/Makefile

@@ -1,5 +1,5 @@
 # PLATFORMS := darwin/amd64 darwin/arm64 freebsd/amd64 linux/386 linux/amd64 linux/arm linux/arm64 linux/mipsle windows/386 windows/amd64 windows/arm windows/arm64
-PLATFORMS := darwin/amd64 darwin/arm64 linux/amd64 linux/arm linux/arm64 linux/mipsle  linux/riscv64 windows/amd64 windows/arm64
+PLATFORMS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/mipsle  linux/riscv64 windows/amd64 windows/arm64
 temp = $(subst /, ,$@)
 os = $(word 1, $(temp))
 arch = $(word 2, $(temp))
@@ -18,7 +18,7 @@ clean:
 
 $(PLATFORMS):
 	@echo "Building $(os)/$(arch)"
-	GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) go build -o './dist/arozos_$(os)_$(arch)'  -ldflags "-s -w" -trimpath
+	GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) GOARM=6 go build -o './dist/arozos_$(os)_$(arch)'  -ldflags "-s -w" -trimpath
 
 fixwindows:
 	-mv ./dist/arozos_windows_amd64 ./dist/arozos_windows_amd64.exe

+ 20 - 0
src/auth.go

@@ -105,4 +105,24 @@ func AuthSettingsInit() {
 
 	//Register nightly task for clearup all user retry counter
 	nightlyManager.RegisterNightlyTask(authAgent.ExpDelayHandler.ResetAllUserRetryCounter)
+
+	//Register nightly task for clearup all expired switchable account pools
+	nightlyManager.RegisterNightlyTask(authAgent.SwitchableAccountManager.RunNightlyCleanup)
+
+	/*
+		Account switching functions
+	*/
+
+	//Register the APIs for account switching functions
+	userRouter := prout.NewModuleRouter(prout.RouterOption{
+		AdminOnly:   false,
+		UserHandler: userHandler,
+		DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
+			utils.SendErrorResponse(w, "Permission Denied")
+		},
+	})
+
+	userRouter.HandleFunc("/system/auth/u/list", authAgent.SwitchableAccountManager.HandleSwitchableAccountListing)
+	userRouter.HandleFunc("/system/auth/u/switch", authAgent.SwitchableAccountManager.HandleAccountSwitch)
+	userRouter.HandleFunc("/system/auth/u/logoutAll", authAgent.SwitchableAccountManager.HandleLogoutAllAccounts)
 }

+ 26 - 2
src/desktop.go

@@ -374,7 +374,7 @@ func desktop_handleUserInfo(w http.ResponseWriter, r *http.Request) {
 	nic, _ := utils.PostPara(r, "noicon")
 	noicon := (nic == "true")
 
-	type returnStruct struct {
+	type PublicUserInfo struct {
 		Username          string
 		UserIcon          string
 		UserGroups        []string
@@ -383,6 +383,30 @@ func desktop_handleUserInfo(w http.ResponseWriter, r *http.Request) {
 		StorageQuotaLeft  int64
 	}
 
+	//Check if the user is requesting another user's public info
+	targetUser, err := utils.GetPara(r, "target")
+	if err == nil {
+		//User asking for another user's desktop icon
+		userIcon := ""
+		searchingUser, err := userHandler.GetUserInfoFromUsername(targetUser)
+		if err != nil {
+			utils.SendErrorResponse(w, "User not found")
+			return
+		}
+
+		//Load the profile image
+		userIcon = searchingUser.GetUserIcon()
+
+		js, _ := json.Marshal(PublicUserInfo{
+			Username: searchingUser.Username,
+			UserIcon: userIcon,
+			IsAdmin:  searchingUser.IsAdmin(),
+		})
+
+		utils.SendJSONResponse(w, string(js))
+		return
+	}
+
 	//Calculate the storage quota left
 	remainingQuota := userinfo.StorageQuota.TotalStorageQuota - userinfo.StorageQuota.UsedStorageQuota
 	if userinfo.StorageQuota.TotalStorageQuota == -1 {
@@ -395,7 +419,7 @@ func desktop_handleUserInfo(w http.ResponseWriter, r *http.Request) {
 		pgs = append(pgs, pg.Name)
 	}
 
-	rs := returnStruct{
+	rs := PublicUserInfo{
 		Username:          userinfo.Username,
 		UserIcon:          userinfo.GetUserIcon(),
 		IsAdmin:           userinfo.IsAdmin(),

+ 1 - 1
src/main.flags.go

@@ -29,7 +29,7 @@ var subserviceBasePort = 12810            //Next subservice port
 
 // =========== SYSTEM BUILD INFORMATION ==============
 var build_version = "development"                      //System build flag, this can be either {development / production / stable}
-var internal_version = "0.2.016"                       //Internal build version, [fork_id].[major_release_no].[minor_release_no]
+var internal_version = "0.2.017"                       //Internal build version, [fork_id].[major_release_no].[minor_release_no]
 var deviceUUID string                                  //The device uuid of this host
 var deviceVendor = "IMUSLAB.INC"                       //Vendor of the system
 var deviceVendorURL = "http://imuslab.com"             //Vendor contact information

+ 1 - 13
src/mediaServer.go

@@ -46,7 +46,7 @@ func mediaServer_init() {
 	http.HandleFunc("/media/download/", serverMedia)
 }
 
-//This function validate the incoming media request and return fsh, vpath, rpath and err if any
+// This function validate the incoming media request and return fsh, vpath, rpath and err if any
 func media_server_validateSourceFile(w http.ResponseWriter, r *http.Request) (*filesystem.FileSystemHandler, string, string, error) {
 	username, err := authAgent.GetUserName(w, r)
 	if err != nil {
@@ -175,18 +175,6 @@ func serverMedia(w http.ResponseWriter, r *http.Request) {
 		}
 		filename := filepath.Base(escapedRealFilepath)
 
-		/*
-			//12 Jul 2022 Update: Deprecated the browser detection logic
-			userAgent := r.Header.Get("User-Agent")
-			if strings.Contains(userAgent, "Safari/")) {
-				//This is Safari. Use speial header
-				w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(realFilepath))
-			} else {
-				//Fixing the header issue on Golang url encode lib problems
-				w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+filename)
-			}
-		*/
-
 		w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
 		w.Header().Set("Content-Type", compatibility.BrowserCompatibilityOverrideContentType(r.UserAgent(), filename, r.Header.Get("Content-Type")))
 		if targetFsh.RequireBuffer || !filesystem.FileExists(realFilepath) {

+ 488 - 0
src/mod/auth/accountSwitch.go

@@ -0,0 +1,488 @@
+package auth
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"log"
+	"net/http"
+	"time"
+
+	"github.com/gorilla/sessions"
+	uuid "github.com/satori/go.uuid"
+	"imuslab.com/arozos/mod/database"
+	"imuslab.com/arozos/mod/utils"
+)
+
+/*
+	Account Switch
+
+	This script handle account switching logic
+
+	The switchable account pools work like this
+	Let say user A want to switch to user B account
+
+	A will create a pool with user A and B username inside the pool
+	The pool UUID will be returned to the client, and stored in local storage
+
+	The client can always switch between A and B as both are in the pool and the
+	client is logged in either A or B's account.
+*/
+
+type SwitchableAccount struct {
+	Username   string //Username of the account
+	LastSwitch int64  //Last time this account is accessed
+}
+
+type SwitchableAccountsPool struct {
+	UUID     string               //UUID of this pool, one pool per browser instance
+	Creator  string               //The user who created the pool. When logout, the pool is discarded
+	Accounts []*SwitchableAccount //Accounts that is cross switchable in this pool
+	parent   *SwitchableAccountPoolManager
+}
+
+type SwitchableAccountPoolManager struct {
+	SessionStore *sessions.CookieStore
+	SessionName  string
+	Database     *database.Database
+	ExpireTime   int64 //Expire time of the switchable account
+	authAgent    *AuthAgent
+}
+
+// Create a new switchable account pool manager
+func NewSwitchableAccountPoolManager(sysdb *database.Database, parent *AuthAgent, key []byte) *SwitchableAccountPoolManager {
+	//Create new database table
+	sysdb.NewTable("auth_acswitch")
+
+	//Create new session store
+	thisManager := SwitchableAccountPoolManager{
+		SessionStore: sessions.NewCookieStore(key),
+		SessionName:  "ao_acc",
+		Database:     sysdb,
+		ExpireTime:   604800,
+		authAgent:    parent,
+	}
+
+	//Do an initialization cleanup
+	go func() {
+		thisManager.RunNightlyCleanup()
+	}()
+
+	//Return the manager
+	return &thisManager
+}
+
+// When called, this will clear the account switching pool in which all users session has expired
+func (m *SwitchableAccountPoolManager) RunNightlyCleanup() {
+	pools, err := m.GetAllPools()
+	if err != nil {
+		log.Println("[auth] Unable to load account switching pools. Cleaning skipped: " + err.Error())
+		return
+	}
+
+	for _, pool := range pools {
+		pool.DeletePoolIfAllUserSessionExpired()
+	}
+}
+
+// Handle switchable account listing for this browser
+func (m *SwitchableAccountPoolManager) HandleSwitchableAccountListing(w http.ResponseWriter, r *http.Request) {
+	//Get username and pool id
+	currentUsername, err := m.authAgent.GetUserName(w, r)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	session, _ := m.SessionStore.Get(r, m.SessionName)
+	poolid, ok := session.Values["poolid"].(string)
+	if !ok {
+		utils.SendErrorResponse(w, "invalid pool id given")
+		return
+	}
+
+	//Check pool exists
+	targetPool, err := m.GetPoolByID(poolid)
+	if err != nil {
+		//Pool expired. Unset the session
+		session.Values["poolid"] = nil
+		session.Save(r, w)
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	//Check if the user can access this pool
+	if !targetPool.IsAccessibleBy(currentUsername) {
+		//Unset the session
+		session.Values["poolid"] = nil
+		session.Save(r, w)
+		utils.SendErrorResponse(w, "access denied")
+		return
+	}
+
+	//Update the user Last Switch Time
+	targetPool.UpdateUserLastSwitchTime(currentUsername)
+
+	//OK. List all the information about the pool
+	type AccountInfo struct {
+		Username  string
+		IsExpired bool
+	}
+
+	results := []*AccountInfo{}
+	for _, acc := range targetPool.Accounts {
+		results = append(results, &AccountInfo{
+			Username:  acc.Username,
+			IsExpired: (time.Now().Unix() > acc.LastSwitch+m.ExpireTime),
+		})
+	}
+	js, _ := json.Marshal(results)
+	utils.SendJSONResponse(w, string(js))
+}
+
+// Handle logout of the current user, return the fallback user if any
+func (m *SwitchableAccountPoolManager) HandleLogoutforUser(w http.ResponseWriter, r *http.Request) (string, error) {
+	currentUsername, err := m.authAgent.GetUserName(w, r)
+	if err != nil {
+		return "", err
+	}
+
+	session, _ := m.SessionStore.Get(r, m.SessionName)
+	poolid, ok := session.Values["poolid"].(string)
+	if !ok {
+		return "", errors.New("user not in a any switchable account pool")
+	}
+
+	//Get the target pool
+	targetpool, err := m.GetPoolByID(poolid)
+	if err != nil {
+		return "", err
+	}
+
+	//Remove the user from the pool
+	targetpool.RemoveUser(currentUsername)
+
+	//Check if the logout user is the creator. If yes, remove the pool
+	if targetpool.Creator == currentUsername {
+		targetpool.Delete()
+
+		//Unset the session
+		session.Values["poolid"] = nil
+		session.Save(r, w)
+
+		return "", nil
+	}
+
+	//return the creator so after logout, the client is switched back to the master account
+	return targetpool.Creator, nil
+}
+
+// Logout all the accounts in the pool
+func (m *SwitchableAccountPoolManager) HandleLogoutAllAccounts(w http.ResponseWriter, r *http.Request) {
+	currentUsername, err := m.authAgent.GetUserName(w, r)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	session, _ := m.SessionStore.Get(r, m.SessionName)
+	poolid, ok := session.Values["poolid"].(string)
+	if !ok {
+		utils.SendErrorResponse(w, "invalid pool id given")
+		return
+	}
+
+	//Get the target pool
+	targetpool, err := m.GetPoolByID(poolid)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	if !targetpool.IsAccessibleBy(currentUsername) {
+		utils.SendErrorResponse(w, "permission denied")
+		return
+	}
+
+	//Remove the pool
+	targetpool.Delete()
+
+	//Unset the session
+	session.Values["poolid"] = nil
+	session.Save(r, w)
+
+	utils.SendOK(w)
+}
+
+// Handle account switching
+func (m *SwitchableAccountPoolManager) HandleAccountSwitch(w http.ResponseWriter, r *http.Request) {
+	previousUserName, err := m.authAgent.GetUserName(w, r)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	session, _ := m.SessionStore.Get(r, m.SessionName)
+	poolid, ok := session.Values["poolid"].(string)
+	if !ok {
+		//No pool is given. Generate a pool for this request
+		poolid = uuid.NewV4().String()
+		newPool := SwitchableAccountsPool{
+			UUID:    poolid,
+			Creator: previousUserName,
+			Accounts: []*SwitchableAccount{
+				{
+					Username:   previousUserName,
+					LastSwitch: time.Now().Unix(),
+				},
+			},
+			parent: m,
+		}
+
+		newPool.Save()
+
+		session.Values["poolid"] = poolid
+		session.Options = &sessions.Options{
+			MaxAge: 3600 * 24 * 30, //One month
+			Path:   "/",
+		}
+		session.Save(r, w)
+	}
+
+	//Get switchable pool from manager
+	targetPool, err := m.GetPoolByID(poolid)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	//Check if this user can access this pool
+	if !targetPool.IsAccessibleByRequest(w, r) {
+		utils.SendErrorResponse(w, "access request denied: user not belongs to this account pool")
+		return
+	}
+
+	//OK! Switch the user to alternative account
+	username, err := utils.PostPara(r, "username")
+	if err != nil {
+		utils.SendErrorResponse(w, "invalid or empty username given")
+		return
+	}
+	password, err := utils.PostPara(r, "password")
+	if err != nil {
+		//Password not given. Check for direct switch
+		switchToTargetAlreadySwitchedBefore := targetPool.UserAlreadyInPool(username)
+		if !switchToTargetAlreadySwitchedBefore {
+			utils.SendErrorResponse(w, "account must be added before it can switch without password")
+			return
+		}
+
+		//Check if the switching is expired
+		lastSwitchTime := targetPool.GetLastSwitchTimeFromUsername(username)
+		if time.Now().Unix() > lastSwitchTime+m.ExpireTime {
+			//Already expired
+			utils.SendErrorResponse(w, "target account session has expired")
+			return
+		}
+
+		//Not expired. Switch over directly
+		m.authAgent.LoginUserByRequest(w, r, username, true)
+	} else {
+		//Password given. Use Add User Account routine
+		ok, reason := m.authAgent.ValidateUsernameAndPasswordWithReason(username, password)
+		if !ok {
+			utils.SendErrorResponse(w, reason)
+			return
+		}
+
+		m.authAgent.LoginUserByRequest(w, r, username, true)
+
+	}
+
+	//Update the pool account info
+	targetPool.UpdateUserPoolAccountInfo(username)
+	targetPool.Save()
+
+	js, _ := json.Marshal(poolid)
+	utils.SendJSONResponse(w, string(js))
+
+	//Debug print
+	//js, _ = json.MarshalIndent(targetPool, "", " ")
+	//fmt.Println("Switching Pool Updated", string(js))
+}
+
+func (m *SwitchableAccountPoolManager) GetAllPools() ([]*SwitchableAccountsPool, error) {
+	results := []*SwitchableAccountsPool{}
+	entries, err := m.Database.ListTable("auth_acswitch")
+	if err != nil {
+		return results, err
+	}
+	for _, keypairs := range entries {
+		//thisPoolID := string(keypairs[0])
+		thisPool := SwitchableAccountsPool{}
+		err = json.Unmarshal(keypairs[1], &thisPool)
+		if err == nil {
+			thisPool.parent = m
+			results = append(results, &thisPool)
+		}
+	}
+
+	return results, nil
+}
+
+// Get a switchable account pool by its id
+func (m *SwitchableAccountPoolManager) GetPoolByID(uuid string) (*SwitchableAccountsPool, error) {
+	targetPool := SwitchableAccountsPool{}
+	err := m.authAgent.Database.Read("auth_acswitch", uuid, &targetPool)
+	if err != nil {
+		return nil, errors.New("pool with given uuid not found")
+	}
+	targetPool.parent = m
+	return &targetPool, nil
+}
+
+// Remove user from all switch pool, which should be called when a user is logged out or removed
+func (p *SwitchableAccountPoolManager) RemoveUserFromAllSwitchableAccountPool(username string) error {
+	allAccountPool, err := p.GetAllPools()
+	if err != nil {
+		return err
+	}
+	for _, accountPool := range allAccountPool {
+		if accountPool.IsAccessibleBy(username) {
+			//aka this user is in the pool
+			accountPool.RemoveUser(username)
+		}
+	}
+	return nil
+}
+
+func (p *SwitchableAccountPoolManager) ExpireUserFromAllSwitchableAccountPool(username string) error {
+	allAccountPool, err := p.GetAllPools()
+	if err != nil {
+		return err
+	}
+	for _, accountPool := range allAccountPool {
+		fmt.Println(allAccountPool)
+		if accountPool.IsAccessibleBy(username) {
+			//aka this user is in the pool
+			accountPool.ExpireUser(username)
+		}
+	}
+	return nil
+}
+
+/*
+	Switachable Account Pool functions
+*/
+
+// Check if the requester can switch within target pool
+func (p *SwitchableAccountsPool) IsAccessibleByRequest(w http.ResponseWriter, r *http.Request) bool {
+	username, err := p.parent.authAgent.GetUserName(w, r)
+	if err != nil {
+		return false
+	}
+	return p.IsAccessibleBy(username)
+}
+
+// Check if a given username can switch within this pool
+func (p *SwitchableAccountsPool) IsAccessibleBy(username string) bool {
+	for _, account := range p.Accounts {
+		if account.Username == username {
+			return true
+		}
+	}
+	return false
+}
+
+func (p *SwitchableAccountsPool) UserAlreadyInPool(username string) bool {
+	for _, acc := range p.Accounts {
+		if acc.Username == username {
+			return true
+		}
+	}
+	return false
+}
+
+func (p *SwitchableAccountsPool) UpdateUserLastSwitchTime(username string) bool {
+	for _, acc := range p.Accounts {
+		if acc.Username == username {
+			acc.LastSwitch = time.Now().Unix()
+		}
+	}
+	return false
+}
+
+func (p *SwitchableAccountsPool) GetLastSwitchTimeFromUsername(username string) int64 {
+	for _, acc := range p.Accounts {
+		if acc.Username == username {
+			return acc.LastSwitch
+		}
+	}
+	return 0
+}
+
+// Everytime switching to a given user in a pool, call this update function to
+// update contents inside the pool
+func (p *SwitchableAccountsPool) UpdateUserPoolAccountInfo(username string) {
+	if !p.UserAlreadyInPool(username) {
+		p.Accounts = append(p.Accounts, &SwitchableAccount{
+			Username:   username,
+			LastSwitch: time.Now().Unix(),
+		})
+	} else {
+		p.UpdateUserLastSwitchTime(username)
+	}
+}
+
+// Expire the session of a user manually
+func (p *SwitchableAccountsPool) ExpireUser(username string) {
+	for _, acc := range p.Accounts {
+		if acc.Username == username {
+			acc.LastSwitch = 0
+		}
+	}
+	p.Save()
+}
+
+// Remove a user from the pool
+func (p *SwitchableAccountsPool) RemoveUser(username string) {
+	newAccountList := []*SwitchableAccount{}
+	for _, acc := range p.Accounts {
+		if acc.Username != username {
+			newAccountList = append(newAccountList, acc)
+		}
+	}
+
+	p.Accounts = newAccountList
+	p.Save()
+}
+
+// Save changes of this pool to database
+func (p *SwitchableAccountsPool) DeletePoolIfAllUserSessionExpired() {
+	allExpred := true
+	for _, acc := range p.Accounts {
+		if !p.IsAccountExpired(acc) {
+			allExpred = false
+		}
+	}
+
+	if allExpred {
+		//All account expired. Remove this pool
+		p.Delete()
+	}
+}
+
+// Save changes of this pool to database
+func (p *SwitchableAccountsPool) Save() {
+	p.parent.Database.Write("auth_acswitch", p.UUID, p)
+}
+
+// Delete this pool from database
+func (p *SwitchableAccountsPool) Delete() error {
+	return p.parent.Database.Delete("auth_acswitch", p.UUID)
+}
+
+// Check if an account is expired
+func (p *SwitchableAccountsPool) IsAccountExpired(acc *SwitchableAccount) bool {
+	return time.Now().Unix() > acc.LastSwitch+p.parent.ExpireTime
+}

+ 49 - 26
src/mod/auth/auth.go

@@ -66,6 +66,9 @@ type AuthAgent struct {
 	WhitelistManager *whitelist.WhiteList
 	BlacklistManager *blacklist.BlackList
 
+	//Account Switcher
+	SwitchableAccountManager *SwitchableAccountPoolManager
+
 	//Logger
 	Logger *authlogger.Logger
 }
@@ -78,7 +81,7 @@ type AuthEndpoints struct {
 	Autologin     string
 }
 
-//Constructor
+// Constructor
 func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, loginRedirectionHandler func(http.ResponseWriter, *http.Request)) *AuthAgent {
 	store := sessions.NewCookieStore(key)
 	err := sysdb.NewTable("auth")
@@ -125,9 +128,14 @@ func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database,
 		WhitelistManager: thisWhitelistManager,
 		BlacklistManager: thisBlacklistManager,
 		ExpDelayHandler:  expLoginHandler,
-		Logger:           newLogger,
+
+		//Switchable Account Pool Manager
+		Logger: newLogger,
 	}
 
+	poolManager := NewSwitchableAccountPoolManager(sysdb, &newAuthAgent, key)
+	newAuthAgent.SwitchableAccountManager = poolManager
+
 	//Create a timer to listen to its token storage
 	go func(listeningAuthAgent *AuthAgent) {
 		for {
@@ -144,7 +152,7 @@ func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database,
 	return &newAuthAgent
 }
 
-//Close the authAgent listener
+// Close the authAgent listener
 func (a *AuthAgent) Close() {
 	//Stop the token listening
 	a.terminateTokenListener <- true
@@ -153,7 +161,7 @@ func (a *AuthAgent) Close() {
 	a.Logger.Close()
 }
 
-//This function will handle an http request and redirect to the given login address if not logged in
+// This function will handle an http request and redirect to the given login address if not logged in
 func (a *AuthAgent) HandleCheckAuth(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
 	if a.CheckAuth(r) {
 		//User already logged in
@@ -164,7 +172,7 @@ func (a *AuthAgent) HandleCheckAuth(w http.ResponseWriter, r *http.Request, hand
 	}
 }
 
-//Handle login request, require POST username and password
+// Handle login request, require POST username and password
 func (a *AuthAgent) HandleLogin(w http.ResponseWriter, r *http.Request) {
 
 	//Get username from request using POST mode
@@ -242,7 +250,7 @@ func (a *AuthAgent) ValidateUsernameAndPassword(username string, password string
 	return succ
 }
 
-//validate the username and password, return reasons if the auth failed
+// validate the username and password, return reasons if the auth failed
 func (a *AuthAgent) ValidateUsernameAndPasswordWithReason(username string, password string) (bool, string) {
 	hashedPassword := Hash(password)
 	var passwordInDB string
@@ -260,7 +268,7 @@ func (a *AuthAgent) ValidateUsernameAndPasswordWithReason(username string, passw
 	}
 }
 
-//Validate the user request for login
+// Validate the user request for login, return true if the target request original is not blocked
 func (a *AuthAgent) ValidateLoginRequest(w http.ResponseWriter, r *http.Request) (bool, error) {
 	//Get the ip address of the request
 	clientIP, err := network.GetIpFromRequest(r)
@@ -287,7 +295,7 @@ func (a *AuthAgent) ValidateLoginIpAccess(ipv4 string) (bool, error) {
 	return true, nil
 }
 
-//Login the user by creating a valid session for this user
+// Login the user by creating a valid session for this user
 func (a *AuthAgent) LoginUserByRequest(w http.ResponseWriter, r *http.Request, username string, rememberme bool) {
 	session, _ := a.SessionStore.Get(r, a.SessionName)
 
@@ -296,7 +304,7 @@ func (a *AuthAgent) LoginUserByRequest(w http.ResponseWriter, r *http.Request, u
 	session.Values["rememberMe"] = rememberme
 
 	//Check if remember me is clicked. If yes, set the maxage to 1 week.
-	if rememberme == true {
+	if rememberme {
 		session.Options = &sessions.Options{
 			MaxAge: 3600 * 24 * 7, //One week
 			Path:   "/",
@@ -310,12 +318,16 @@ func (a *AuthAgent) LoginUserByRequest(w http.ResponseWriter, r *http.Request, u
 	session.Save(r, w)
 }
 
-//Handle logout, reply OK after logged out. WILL NOT DO REDIRECTION
+// Handle logout, reply OK after logged out. WILL NOT DO REDIRECTION
 func (a *AuthAgent) HandleLogout(w http.ResponseWriter, r *http.Request) {
 	username, _ := a.GetUserName(w, r)
 	if username != "" {
 		log.Println(username + " logged out.")
 	}
+
+	//Clear user switchable account pools
+	fallbackAccount, _ := a.SwitchableAccountManager.HandleLogoutforUser(w, r)
+
 	// Revoke users authentication
 	err := a.Logout(w, r)
 	if err != nil {
@@ -323,6 +335,11 @@ func (a *AuthAgent) HandleLogout(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	if fallbackAccount != "" {
+		//Switch to fallback account
+		a.LoginUserByRequest(w, r, fallbackAccount, true)
+	}
+
 	w.Write([]byte("OK"))
 }
 
@@ -334,10 +351,11 @@ func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
 	session.Values["authenticated"] = false
 	session.Values["username"] = nil
 	session.Save(r, w)
+
 	return nil
 }
 
-//Get the current session username from request
+// Get the current session username from request
 func (a *AuthAgent) GetUserName(w http.ResponseWriter, r *http.Request) (string, error) {
 	if a.CheckAuth(r) {
 		//This user has logged in.
@@ -349,16 +367,16 @@ func (a *AuthAgent) GetUserName(w http.ResponseWriter, r *http.Request) (string,
 	}
 }
 
-//Check if the user has logged in, return true / false in JSON
+// Check if the user has logged in, return true / false in JSON
 func (a *AuthAgent) CheckLogin(w http.ResponseWriter, r *http.Request) {
-	if a.CheckAuth(r) != false {
+	if a.CheckAuth(r) {
 		sendJSONResponse(w, "true")
 	} else {
 		sendJSONResponse(w, "false")
 	}
 }
 
-//Handle new user register. Require POST username, password, group.
+// Handle new user register. Require POST username, password, group.
 func (a *AuthAgent) HandleRegister(w http.ResponseWriter, r *http.Request) {
 	userCount := a.GetUserCounts()
 
@@ -407,7 +425,7 @@ func (a *AuthAgent) HandleRegister(w http.ResponseWriter, r *http.Request) {
 	return
 }
 
-//Check authentication from request header's session value
+// Check authentication from request header's session value
 func (a *AuthAgent) CheckAuth(r *http.Request) bool {
 	session, _ := a.SessionStore.Get(r, a.SessionName)
 	// Check if user is authenticated
@@ -417,11 +435,11 @@ func (a *AuthAgent) CheckAuth(r *http.Request) bool {
 	return true
 }
 
-//Handle de-register of users. Require POST username.
-//THIS FUNCTION WILL NOT CHECK FOR PERMISSION. PLEASE USE WITH PERMISSION HANDLER
+// Handle de-register of users. Require POST username.
+// THIS FUNCTION WILL NOT CHECK FOR PERMISSION. PLEASE USE WITH PERMISSION HANDLER
 func (a *AuthAgent) HandleUnregister(w http.ResponseWriter, r *http.Request) {
 	//Check if the user is logged in
-	if a.CheckAuth(r) == false {
+	if !a.CheckAuth(r) {
 		//This user has not logged in
 		sendErrorResponse(w, "Login required to remove user from the system.")
 		return
@@ -469,10 +487,13 @@ func (a *AuthAgent) UnregisterUser(username string) error {
 
 	//Remove the user's autologin tokens
 	a.RemoveAutologinTokenByUsername(username)
+
+	//Remove user from switchable accounts
+	a.SwitchableAccountManager.RemoveUserFromAllSwitchableAccountPool(username)
 	return nil
 }
 
-//Get the number of users in the system
+// Get the number of users in the system
 func (a *AuthAgent) GetUserCounts() int {
 	entries, _ := a.Database.ListTable("auth")
 	usercount := 0
@@ -489,7 +510,7 @@ func (a *AuthAgent) GetUserCounts() int {
 	return usercount
 }
 
-//List all username within the system
+// List all username within the system
 func (a *AuthAgent) ListUsers() []string {
 	entries, _ := a.Database.ListTable("auth")
 	results := []string{}
@@ -502,7 +523,7 @@ func (a *AuthAgent) ListUsers() []string {
 	return results
 }
 
-//Check if the given username exists
+// Check if the given username exists
 func (a *AuthAgent) UserExists(username string) bool {
 	userpasswordhash := ""
 	err := a.Database.Read("auth", "passhash/"+username, &userpasswordhash)
@@ -512,14 +533,14 @@ func (a *AuthAgent) UserExists(username string) bool {
 	return true
 }
 
-//Update the session expire time given the request header.
+// Update the session expire time given the request header.
 func (a *AuthAgent) UpdateSessionExpireTime(w http.ResponseWriter, r *http.Request) bool {
 	session, _ := a.SessionStore.Get(r, a.SessionName)
-	if session.Values["authenticated"].(bool) == true {
+	if session.Values["authenticated"].(bool) {
 		//User authenticated. Extend its expire time
 		rememberme := session.Values["rememberMe"].(bool)
 		//Extend the session expire time
-		if rememberme == true {
+		if rememberme {
 			session.Options = &sessions.Options{
 				MaxAge: 3600 * 24 * 7, //One week
 				Path:   "/",
@@ -537,14 +558,16 @@ func (a *AuthAgent) UpdateSessionExpireTime(w http.ResponseWriter, r *http.Reque
 	}
 }
 
-//Create user account
+// Create user account
 func (a *AuthAgent) CreateUserAccount(newusername string, password string, group []string) error {
 	key := newusername
+
 	hashedPassword := Hash(password)
 	err := a.Database.Write("auth", "passhash/"+key, hashedPassword)
 	if err != nil {
 		return err
 	}
+
 	//Store this user's usergroup settings
 	err = a.Database.Write("auth", "group/"+newusername, group)
 	if err != nil {
@@ -553,7 +576,7 @@ func (a *AuthAgent) CreateUserAccount(newusername string, password string, group
 	return nil
 }
 
-//Hash the given raw string into sha512 hash
+// Hash the given raw string into sha512 hash
 func Hash(raw string) string {
 	h := sha512.New()
 	h.Write([]byte(raw))

+ 11 - 4
src/mod/disk/smart/smart.go

@@ -14,6 +14,7 @@ import (
 	"encoding/json"
 	"log"
 	"net/http"
+	"os"
 	"strconv"
 	"strings"
 
@@ -44,6 +45,12 @@ func NewSmartListener() (*SMARTListener, error) {
 		return &SMARTListener{}, errors.New("smartctl not found")
 	}
 
+	//Updated 5 June 2023: Try to chmod it if it is on linux so that
+	//broken permissions still works in sudo mode
+	if runtime.GOOS == "linux" {
+		os.Chmod(smartExec, 0777)
+	}
+
 	driveList := scanAvailableDevices(smartExec)
 	readSMARTDevices(smartExec, &driveList)
 	fillHealthyStatus(&driveList)
@@ -54,7 +61,7 @@ func NewSmartListener() (*SMARTListener, error) {
 	}, nil
 }
 
-//this function used for fetch available devices by using smartctl
+// this function used for fetch available devices by using smartctl
 func scanAvailableDevices(smartExec string) DevicesList {
 	rawInfo := execCommand(smartExec, "--scan", "--json=c")
 	devicesList := new(DevicesList)
@@ -70,7 +77,7 @@ func scanAvailableDevices(smartExec string) DevicesList {
 	return *devicesList
 }
 
-//this function used for merge SMART Information into devicesList
+// this function used for merge SMART Information into devicesList
 func readSMARTDevices(smartExec string, devicesList *DevicesList) {
 	for i, device := range devicesList.Devices {
 		rawInfo := execCommand(smartExec, device.Name, "--info", "--all", "--json=c")
@@ -80,7 +87,7 @@ func readSMARTDevices(smartExec string, devicesList *DevicesList) {
 	}
 }
 
-//used for fill the healthy status to the array
+// used for fill the healthy status to the array
 func fillHealthyStatus(devicesList *DevicesList) {
 	devicesList.Healthy = "Normal"
 	for i, device := range devicesList.Devices {
@@ -103,7 +110,7 @@ func fillHealthyStatus(devicesList *DevicesList) {
 	}
 }
 
-//fill the capacity if windows
+// fill the capacity if windows
 func fillCapacity(devicesList *DevicesList) {
 	if runtime.GOOS == "windows" {
 		DiskNames := wmicGetinfo("diskdrive", "Model")

+ 6 - 2
src/mod/filesystem/abstractions/webdavfs/webdavfs.go

@@ -161,7 +161,7 @@ func (e WebDAVFileSystem) IsDir(filename string) bool {
 	return s.IsDir()
 }
 
-//Notes: This is not actual Glob function. This just emulate Glob using ReadDir with max depth 1 layer
+// Notes: This is not actual Glob function. This just emulate Glob using ReadDir with max depth 1 layer
 func (e WebDAVFileSystem) Glob(wildcard string) ([]string, error) {
 	wildcard = filepath.ToSlash(filepath.Clean(wildcard))
 
@@ -172,8 +172,8 @@ func (e WebDAVFileSystem) Glob(wildcard string) ([]string, error) {
 	chunks := strings.Split(strings.TrimPrefix(wildcard, "/"), "/")
 	results, err := e.globpath("/", chunks, 0)
 	return results, err
-
 }
+
 func (e WebDAVFileSystem) GetFileSize(filename string) int64 {
 	filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
 	s, err := e.Stat(filename)
@@ -184,6 +184,7 @@ func (e WebDAVFileSystem) GetFileSize(filename string) int64 {
 
 	return s.Size()
 }
+
 func (e WebDAVFileSystem) GetModTime(filename string) (int64, error) {
 	filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
 	s, err := e.Stat(filename)
@@ -193,10 +194,12 @@ func (e WebDAVFileSystem) GetModTime(filename string) (int64, error) {
 
 	return s.ModTime().Unix(), nil
 }
+
 func (e WebDAVFileSystem) WriteFile(filename string, content []byte, mode os.FileMode) error {
 	filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
 	return e.c.Write(filename, content, mode)
 }
+
 func (e WebDAVFileSystem) ReadFile(filename string) ([]byte, error) {
 	filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
 	bytes, err := e.c.Read(filename)
@@ -205,6 +208,7 @@ func (e WebDAVFileSystem) ReadFile(filename string) ([]byte, error) {
 	}
 	return bytes, nil
 }
+
 func (e WebDAVFileSystem) ReadDir(filename string) ([]fs.DirEntry, error) {
 	filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
 	fis, err := e.c.ReadDir(filename)

+ 1 - 1
src/mod/user/useropr.go

@@ -17,7 +17,7 @@ func (u *User) RemoveUser() {
 	u.parent.authAgent.UnregisterUser(u.Username)
 }
 
-//Get the current user icon
+//Get the target user icon
 func (u *User) GetUserIcon() string {
 	var userIconpath []byte
 	u.parent.database.Read("auth", "profilepic/"+u.Username, &userIconpath)

+ 6 - 2
src/user.go

@@ -88,7 +88,7 @@ func UserSystemInit() {
 	adminRouter.HandleFunc("/system/users/removeUser", user_handleUserRemove)
 }
 
-//Remove a user from the system
+// Remove a user from the system
 func user_handleUserRemove(w http.ResponseWriter, r *http.Request) {
 	username, err := utils.PostPara(r, "username")
 	if err != nil {
@@ -279,7 +279,7 @@ func user_handleUserEdit(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-//Get the user interface info for the user to launch into
+// Get the user interface info for the user to launch into
 func user_getInterfaceInfo(w http.ResponseWriter, r *http.Request) {
 	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
 	if err != nil {
@@ -349,6 +349,10 @@ func user_handleUserInfo(w http.ResponseWriter, r *http.Request) {
 			utils.SendErrorResponse(w, "Invalid old password.")
 			return
 		}
+
+		//Logout users from all switchable accounts
+		authAgent.SwitchableAccountManager.ExpireUserFromAllSwitchableAccountPool(username)
+
 		//OK! Change user password
 		newHashedPassword := auth.Hash(newpw)
 		sysdb.Write("auth", "passhash/"+username, newHashedPassword)

+ 372 - 0
src/web/SystemAO/advance/switchAccount.html

@@ -0,0 +1,372 @@
+<!DOCTYPE HTML>
+<html>
+    <head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <title>Add Account</title>
+    <link rel="stylesheet" href="../../script/semantic/semantic.css">
+    <link rel="stylesheet" href="../../script/ao.css">
+    <script type="application/javascript" src="../../script/jquery.min.js"></script>
+    <script type="application/javascript" src="../../script/ao_module.js"></script>
+    <script type="application/javascript" src="../../script/semantic/semantic.js"></script>
+    <script type="text/javascript" src="../../script/applocale.js"></script>
+    <style>
+        
+        body{
+            background-color: var(--body_background);
+        }
+        
+
+        .alternativeAccount{
+            cursor: pointer;
+            padding: 0.6em;
+            border: 1px solid rgb(236, 236, 236);
+            border-radius: 0.4em;
+            margin-top: 0.4em;
+        }
+
+        .alternativeAccount.expired{
+            opacity: 0.6;
+        }
+
+        .alternativeAccount:hover{
+            opacity: 0.6;
+        }
+
+        /*
+            Darktheme overwrite
+        */
+
+        body.darkTheme .ui.segment{
+            background-color: var(--body_background_active);
+            color: var(--text_color);
+        }
+
+        body.darkTheme div, body.darkTheme button, body.darkTheme span,  body.darkTheme i{
+            color: var(--text_color);
+        }
+
+        body.darkTheme .ui.header .sub.header{
+            color: var(--text_color_secondary);
+        }
+
+        body.darkTheme .ui.basic.buttons .button,body.darkTheme  .ui.basic.button{
+            color: var(--text_color_secondary);
+            border: 1px solid var(--text_color_secondary);
+        }
+
+        body.darkTheme .ui.basic.buttons .button,body.darkTheme  .ui.basic.button:hover{
+            background-color: var(--body_background_active) !important;
+            opacity: 0.8;
+        }
+
+        body.darkTheme .ui.form .field > label{
+            color: var(--text_color_secondary) !important;
+        }
+
+        body.darkTheme .ui.divider{
+            border-bottom: 1px solid var(--divider) !important;
+        }
+
+        body.darkTheme .ui.form input:not([type]),body.darkTheme .ui.form input[type="date"],body.darkTheme  .ui.form input[type="datetime-local"],body.darkTheme .ui.form input[type="email"],body.darkTheme .ui.form input[type="number"],body.darkTheme .ui.form input[type="password"],body.darkTheme .ui.form input[type="search"], .ui.form input[type="tel"],body.darkTheme .ui.form input[type="time"],body.darkTheme .ui.form input[type="text"],body.darkTheme .ui.form input[type="file"],body.darkTheme .ui.form input[type="url"]{
+            background: var(--body_background_secondary);
+            border: 1px solid var(--text_color_invert);
+            color: var(--text_color);
+        }
+
+        body.darkTheme .ui.message{
+            background-color: #1B1C1D;
+            color: rgba(255, 255, 255, 0.9);
+        }
+
+        body.darkTheme #isAdminLogo{
+            color: var(--text_color) !important;
+        }
+
+        body.darkTheme .alternativeAccount .content i.isAdminIcon{
+            color: var(--text_color) !important;
+        }
+    </style>
+    </head>
+    <body> 
+    <br>
+    <div class="ui container" align="center">
+        <div class="ui segment" style="max-width:400px;" align="left">
+            <!-- Current In Use Account -->
+            <p locale="desc/currentAccount">Current account</p>
+            <div class="ui small basic right floated button" onclick="logout();">
+                <i class="log out icon"></i> <span locale="button/logout">Logout</span>
+            </div>
+            <div class="ui header">
+                <img id="currentUserIcon" src="/images/icons/plugin.png">
+                <div class="content">
+                    <span id="currentUsername"><i class="ui loading spinner icon"></i></span> <i id="isAdminLogo" style="margin-left: 0.4em; color: rgb(38, 50, 56);" title="Admin" class="small shield alternate icon themed text"></i>
+                    <div id="currentUserGroups" class="sub header"></div>
+                </div>
+            </div>
+           
+            <div class="ui divider"></div>
+            <p locale="desc/savedAccount">Saved accounts on this browser</p>
+            <div id="alternativeAccountList">
+
+            </div>
+            
+            
+            <div style="margin-top: 1em !important;">
+                <div id="signoutAllButton" class="ui fluid small black basic button" onclick="logoutAllAccounts();"><i class="log out icon icon"></i> <span locale="desc/signoutAll">Sign-out all accounts</span></div>
+            </div>
+            <div class="ui divider"></div>
+                <p locale="desc/sign-in-new">Sign-in to new account</p>
+            <form class="ui form" onsubmit="handleFormSubmit(event, this);">
+                <div class="field">
+                    <label locale="desc/username">Username</label>
+                    <input id="username" type="text" name="username" value="">
+                </div>
+                <div class="field">
+                    <label locale="desc/password">Password</label>
+                    <input id="magic" type="password" name="magic">
+                </div>
+                <button id="submitbtn" class="ui basic button"><i class="ui green sign in icon"></i> <span locale="desc/addAccount">Add Local Account</span></button>
+            </form>
+            <div id="restoreSessionMessage" class="ui blue inverted segment" style="display:none;">
+                <span locale="desc/enterPassword">Enter password to resume session</span>
+            </div>
+            <div id="errmsg" class="ui red inverted segment" style="display:none;">
+                <i class="remove icon"></i> <span id="errtext">Internal Server Error</span>
+            </div>
+            <br>
+        </div>
+    </div>
+        
+    <script>
+        //Username is just for display purpose. Even if anyone hacked this
+        //and change to another user account, it is still based on the session value
+        //matched by cookie ao_auth on server side
+        let currentUserInfo = {};
+        
+        //Initalized localization
+        if (typeof(applocale) != "undefined"){
+            applocale.init("../../SystemAO/locale/switchAccount.json", function(){
+                applocale.translate();
+                initCurrentAccountInfo(function(){
+                    listAllStoredAccounts();
+                });
+            });
+        }else{
+            //Applocale not found
+            var applocale = {};
+            applocale.getString = function(key, defaultString){
+                return defaultString;
+            }
+
+            initCurrentAccountInfo(function(){
+                listAllStoredAccounts();
+            });
+        }
+
+        //Initialize theme
+        $.get("../../system/file_system/preference?key=file_explorer/theme", function(data){
+            if (data == "darkTheme"){
+                $("body").addClass("darkTheme");
+            }else{
+                $("body").addClass("whiteTheme");
+            }
+        });
+
+        function handleFormSubmit(event, form){
+            event.preventDefault();
+
+            let username = $("#username").val();
+            let password = $("#magic").val();
+
+            //Login to the new account
+            $.ajax({
+                url: "../../system/auth/u/switch",
+                method: "POST",
+                data: {
+                    username: username,
+                    password: password,
+                },
+                success: function(data){
+                    if (data.error != undefined){
+                        $("#errtext").text(data.error);
+                        $("#errmsg").show();
+                    }else{
+                        //Refresh the page
+                        $("#errmsg").hide();
+                        initCurrentAccountInfo(function(){
+                            listAllStoredAccounts();
+                            if(ao_module_virtualDesktop){
+                                parent.initDesktop();
+                            }
+                        });
+                    }
+                    $("#restoreSessionMessage").hide();
+                },
+            });
+        }
+        
+        function switchAccount(object){
+            let targetUsername = $(object).attr("acname");
+            if (targetUsername == undefined || targetUsername == ""){
+                console.log("Unable to load username from element")
+                return;
+            }
+
+            //Check if it is expired
+            if ($(object).hasClass("expired")){
+                $("#username").val(targetUsername);
+                $("#restoreSessionMessage").show();
+                return;
+            }
+
+            $.ajax({
+                url: "../../system/auth/u/switch",
+                data: {
+                    "username": targetUsername,
+                },
+                success: function(data){
+                    if (data.error != undefined){
+                        showError(data.error);
+                    }else{
+                        hideError();
+                        initCurrentAccountInfo(function(){
+                            listAllStoredAccounts();
+                            if(ao_module_virtualDesktop){
+                                parent.initDesktop();
+                            }
+                        });
+                    }
+                }
+            })
+
+        }
+
+        function logoutAllAccounts(){
+            if (confirm(applocale.getString("msg/logout/confirm", "This will logout all other accounts from this browser. Confirm?"))){
+                $.ajax({
+                    url: "/system/auth/u/logoutAll",
+                    success: function(data){
+                        if (data.error != undefined){
+                            showError(data.error);
+                        }else{
+                            //Reset the browser pool id
+                            hideError();
+                            listAllStoredAccounts();
+                            if(ao_module_virtualDesktop){
+                                parent.initDesktop();
+                            }
+                        }
+                    }
+                })
+            }
+        }
+
+        function showError(message){
+            function capitalizeFirstLetter(string) {
+                return string.charAt(0).toUpperCase() + string.slice(1);
+            }
+            $("#errtext").text(capitalizeFirstLetter(message));
+            $("#errmsg").show();
+        }
+
+        function hideError(){
+            $("#errmsg").hide();
+        }
+
+        function listAllStoredAccounts(){
+            $("#alternativeAccountList").empty();
+        
+            //Request server side for the account pool
+            $.get("../../system/auth/u/list", function(data){
+                if (data.error != undefined){
+                    $("#signoutAllButton").addClass('disabled');
+                    $("#alternativeAccountList").append(`<div class="ui message">
+                        <i class="ui green check circle icon"></i> ${applocale.getString("desc/noAlternative", "No other account stored on this browser")}
+                    </div>`);
+                    return;
+                }else{
+                    if (data.length > 0){
+                        data.forEach(function(account){
+                            if (account.Username == currentUserInfo.username){
+                                //Skip
+                                return;
+                            }
+                            
+                            $.get("../../system/desktop/user?target=" + account.Username, function(data){
+                                let userIcon = data.UserIcon;
+                                if (userIcon == ""){
+                                    userIcon = "../../img/desktop/system_icon/user.svg"
+                                }
+                                $("#alternativeAccountList").append(`
+                                    <div class="alternativeAccount ${account.IsExpired?"expired":""}" acname="${account.Username}" onclick="switchAccount(this);">
+                                        <div class="ui header">
+                                            <img class="usericon" src="${userIcon}">
+                                            <div class="content" style="font-size: 95% !important;">
+                                                <span class="username">${account.Username}</span> ${(data.IsAdmin)?'<i style="margin-left: 0.4em; color: rgb(38, 50, 56);" class="small shield alternate icon themed text isAdminIcon"></i>':""}
+                                                <div class="sub header usergroup">${!account.IsExpired?"<i class='ui green check circle icon' style='margin-right: 0px;'></i> " + applocale.getString("desc/sessionValid", "Session Valid"):"<i class='ui red times circle icon' style='margin-right: 0px;'></i> " + applocale.getString("desc/sessionExpired", "Session Expired")}</div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                `);
+                            });
+                        });
+                        $("#signoutAllButton").removeClass('disabled');
+                    }else{
+                        $("#signoutAllButton").addClass('disabled');
+                        $("#alternativeAccountList").append(`<div class="ui message">
+                            <i class="ui green check circle icon"></i> ${applocale.getString("desc/noAlternative", "No other account stored on this browser")}
+                        </div>`);
+                        return;
+                    }
+                }
+            })
+        }
+        
+
+        function initCurrentAccountInfo(callback=undefined){
+            $.get("../../system/desktop/user", function(data){
+                if (data.error != undefined){
+                    alert(data.error);
+                    return
+                }
+
+                let userIcon = data.UserIcon;
+                if (userIcon == ""){
+                    userIcon = "../../img/desktop/system_icon/user.svg"
+                }
+                $("#currentUserIcon").attr("src", userIcon);
+                $("#currentUsername").text(data.Username);
+                $("#currentUserGroups").text("@" + data.UserGroups.join(", "));
+
+                if (data.IsAdmin){
+                    $("#isAdminLogo").show();
+                }else{
+                    $("#isAdminLogo").hide();
+                }
+
+                currentUserInfo = {
+                    "username": data.Username,
+                    "admin": data.IsAdmin,
+                    "groups": data.UserGroups,
+                }
+
+                if (callback != undefined){
+                    callback();
+                }
+                
+            });
+        }
+
+        function logout() {
+            loggingOut = true;
+            if (confirm(applocale.getString("msg/logout/thisAcConfirm", "Exiting Session. Confirm?"))){
+                $.get("../../system/auth/logout", function() {
+                    window.top.location.href = "../../";
+                });
+            }
+            hideAllContextMenus();
+        }
+    </script>
+    </body>
+</html>

二进制
src/web/SystemAO/desktop/img/account-switch.png


二进制
src/web/SystemAO/desktop/img/account-switch.psd


二进制
src/web/SystemAO/desktop/img/switch_account_FILL0_wght400_GRAD0_opsz48.png


二进制
src/web/SystemAO/info/img/banner.png


二进制
src/web/SystemAO/info/img/banner.psd


+ 40 - 3
src/web/SystemAO/locale/desktop.json

@@ -96,7 +96,16 @@
                 "power/restart/text":"重啟伺服器主機",
                 "power/restart/instruction":"請輸入你的管理員密碼以確認重啟伺服器",
 
+                "account/switch/logout/confirm":"確認登出所有其他儲存在本瀏覽器的帳戶?",
+                "account/switch/noAlternative":"此瀏覽器上沒有儲存其他帳戶",
+                "account/switch/sessionValid":"登錄狀態可用",
+                "account/switch/sessionExpired":"登錄狀態已過期",
+                "account/switch/addAccount":"新增本機帳戶",
+                "account/switch/signoutAll":"登出其他已儲存的帳戶",
+
                 "upload/message/uploading": "上載中…",
+                "upload/message/failed": "上載失敗",
+
                 "":""
             },
             "placeholder":{
@@ -194,6 +203,13 @@
                 "power/restart/title":"重新啟動 - 輸入密碼以作確認",
                 "power/restart/text":"重啟伺服器主機",
                 "power/restart/instruction":"請輸入你的管理員密碼以確認重啟伺服器",
+                
+                "account/switch/logout/confirm":"確認登出所有其他儲存在本瀏覽器的帳戶?",
+                "account/switch/noAlternative":"此瀏覽器上沒有儲存其他帳戶",
+                "account/switch/sessionValid":"登錄狀態可用",
+                "account/switch/sessionExpired":"登錄狀態已過期",
+                "account/switch/addAccount":"新增本機帳戶",
+                "account/switch/signoutAll":"登出其他已儲存的帳戶",
 
                 "upload/message/uploading": "上載中…",
                 "upload/message/failed": "上載失敗",
@@ -295,6 +311,13 @@
                 "power/restart/text":"服务器重启",
                 "power/restart/instruction":"请输入你的管理员密码以确认重启服务器",
 
+                "account/switch/logout/confirm": "确认登出所有其他储存在本浏览器的帐户?",
+                "account/switch/noAlternative": "此浏览器上没有储存其他帐户",
+                "account/switch/sessionValid": "登录状态可用",
+                "account/switch/sessionExpired": "登录状态已过期",
+                "account/switch/addAccount": "新增本机帐户",
+                "account/switch/signoutAll": "登出其他已储存的帐户",
+
                 "upload/message/uploading": "上传中…",
                 "upload/message/failed": "上传失败",
                 "":""
@@ -391,11 +414,18 @@
                 
                 "power/shutdown/title": "Shut Down - Enter Password to Confirm",
                 "power/shutdown/text": "Turn off server",
-                "power/shutdown/instruction": "Please enter your admin password to initiate the shut down process.",
+                "power/shutdown/instruction": "Enter your admin password to continue shut down process.",
                 "power/restart/title": "Restart - Enter Password to Confirm",
                 "power/restart/text": "Restart server",
-                "power/restart/instruction": "Please enter your admin password to confirm the server restart.",
-    
+                "power/restart/instruction": "Enter your admin password to continue server restart.",
+                
+                "account/switch/logout/confirm": "Confirm Logout from All Other Accounts Stored in this Browser?",
+                "account/switch/noAlternative": "No Other Logged-in Accounts Found on this Browser",
+                "account/switch/sessionValid": "Session Valid",
+                "account/switch/sessionExpired": "Session Expired",
+                "account/switch/addAccount": "Add another account",
+                "account/switch/signoutAll": "Sign-out all accounts",
+
                 "upload/message/uploading": "Uploading...",
                 "":""
             },
@@ -496,6 +526,13 @@
                 "power/restart/title": "再起動 - パスワードを入力して確認してください",
                 "power/restart/text": "サーバーを再起動します",
                 "power/restart/instruction": "サーバーの再起動を確認するには、管理者パスワードを入力してください。",
+                
+                "account/switch/logout/confirm": "このブラウザに保存されている他のすべてのアカウントからログアウトしますか?",
+                "account/switch/noAlternative": "このブラウザで他のログイン中のアカウントは見つかりません",
+                "account/switch/sessionValid": "セッションが有効です",
+                "account/switch/sessionExpired": "セッションが期限切れです",
+                "account/switch/addAccount": "別のアカウントを追加",
+                "account/switch/signoutAll": "すべてのアカウントからサインアウト",
 
                 "upload/message/uploading": "アップロード中...",
                 "":""

+ 148 - 0
src/web/SystemAO/locale/switchAccount.json

@@ -0,0 +1,148 @@
+{
+    "author": "tobychui",
+    "version": "1.0",
+    "keys": {
+        "zh-tw": {
+            "fwtitle" : "帳戶切換管理員",
+            "strings":{
+                "desc/currentAccount":"使用中的帳戶",
+                "desc/savedAccount":"已在此瀏覽器登入之帳戶",
+                "desc/sign-in-new":"切換至其他帳戶",
+                "desc/username":"用戶名稱",
+                "desc/password":"密碼",
+                "desc/enterPassword":"輸入密碼以恢復階段",
+                "desc/switchAccount": "切換帳戶",
+                "desc/noAlternative":"此瀏覽器上沒有儲存其他帳戶",
+                "desc/sessionValid":"登錄狀態可用",
+                "desc/sessionExpired":"登錄狀態已過期",
+                "desc/addAccount":"新增本機帳戶",
+                "desc/signoutAll":"登出其他已儲存的帳戶",
+                
+                "msg/logout/confirm":"確認登出所有其他儲存在本瀏覽器的帳戶?",
+                "msg/logout/thisAcConfirm": "確認登出本帳戶?",
+                "button/login":"登入",
+                "button/logout":"登出",
+
+
+                "":""
+            },
+            "titles":{
+
+            },
+            "placeholder":{
+
+            }
+        },
+        "zh-hk": {
+            "fwtitle" : "帳戶切換管理員",
+            "strings":{
+                "desc/currentAccount":"使用中的帳戶",
+                "desc/savedAccount":"已在此瀏覽器登入之帳戶",
+                "desc/sign-in-new":"切換至其他帳戶",
+                "desc/username":"用戶名稱",
+                "desc/password":"密碼",
+                "desc/enterPassword":"輸入密碼以恢復階段",
+                "desc/switchAccount": "切換帳戶",
+                "desc/noAlternative":"此瀏覽器上沒有儲存其他帳戶",
+                "desc/sessionValid":"登錄狀態可用",
+                "desc/sessionExpired":"登錄狀態已過期",
+                "desc/addAccount":"新增本機帳戶",
+                "desc/signoutAll":"登出其他已儲存的帳戶",
+
+                "msg/logout/confirm":"確認登出所有其他儲存在本瀏覽器的帳戶?",
+                "msg/logout/thisAcConfirm": "確認登出本帳戶?",
+                "button/login":"登入",
+                "button/logout":"登出",
+                "":""
+            },
+            "titles":{
+
+            },
+            "placeholder":{
+
+            }
+        },
+        "zh-cn": {
+            "fwtitle" : "帐户切换管理员",
+            "strings":{
+                "desc/currentAccount":"使用中的帐户",
+                "desc/savedAccount":"已在此浏览器登入之帐户",
+                "desc/sign-in-new":"切换至其他帐户",
+                "desc/username":"用户名称",
+                "desc/password":"密码",
+                "desc/enterPassword":"输入密码以恢复阶段",
+                "desc/switchAccount": "切换帐户",
+                "desc/noAlternative":"此浏览器上没有储存其他帐户",
+                "desc/sessionValid":"登录状态可用",
+                "desc/sessionExpired":"登录状态已过期",
+                "desc/addAccount":"新增本机帐户",
+                "desc/signoutAll":"登出其他已储存的帐户",
+
+                "msg/logout/confirm":"确认登出所有其他储存在本浏览器的帐户?",
+                "msg/logout/thisAcConfirm": "确认登出本帐户?",
+                "button/login":"登入",
+                "button/logout":"登出",
+                "":""
+            },
+            "titles":{
+
+            },
+            "placeholder":{
+
+            }
+        },
+        "en-us": {
+            "fwtitle" : "Account Manager",
+            "strings":{
+                "desc/currentAccount": "Current Account",
+                "desc/savedAccount": "Saved accounts on this browser",
+                "desc/sign-in-new":"Sign-in to new account",
+                "desc/username": "Username",
+                "desc/password": "Password",
+                "desc/enterPassword": "Enter password to resume session",
+                "desc/switchAccount": "Add Local Account",
+                "desc/noAlternative":"No other account stored on this browser",
+                "desc/sessionValid": "Session Valid",
+                "desc/sessionExpired": "Session Expired",
+                "desc/addAccount": "Add local account",
+                "desc/signoutAll":"Sign out other saved accounts",
+
+                "msg/logout/confirm": "This will logout all other accounts from this browser. Confirm?",
+                "msg/logout/thisAcConfirm": "Exiting Session. Confirm?",
+                "button/login": "login",
+                "button/logout": "Logout",
+                "":""
+            },
+            "titles":{
+
+            },
+            "placeholder":{
+
+            }
+        },
+        "ja-jp":{
+            "name":"日本語",
+            "fwtitle" : "アカウントマネージャー",
+            "fontFamily":"\"Meiryo UI\", \"Arial Unicode MS\", \"Hiragino Kaku Gothic Pro\"",
+            "strings":{
+                "desc/currentAccount": "現在のアカウント",
+                "desc/savedAccount": "このブラウザに保存されたアカウント",
+                "desc/sign-in-new":"新しいアカウントにサインイン",
+                "desc/username": "ユーザー名",
+                "desc/password": "パスワード",
+                "desc/enterPassword": "セッションを再開するためにパスワードを入力してください",
+                "desc/switchAccount": "ローカルアカウントを追加",
+                "desc/noAlternative":"このブラウザに他のアカウントは保存されていません",
+                "desc/sessionValid": "セッション有効",
+                "desc/sessionExpired": "セッションの有効期限が切れました",
+                "desc/addAccount": "ローカルアカウントを追加",
+                "desc/signoutAll":"他の保存されたアカウントからログアウト",
+
+                "msg/logout/confirm": "これにより、このブラウザから他のすべてのアカウントがログアウトされます。確認しますか?",
+                "msg/logout/thisAcConfirm": "セッションを終了します。確認しますか?",
+                "button/login": "ログイン",
+                "button/logout": "ログアウト"
+            }   
+        }
+    }
+}

+ 4 - 1
src/web/SystemAO/locale/template.json

@@ -3,6 +3,7 @@
     "version": "1.0",
     "keys": {
         "zh-tw": {
+            "name": "繁體中文(台灣)",
             "fwtitle" : "",
             "strings":{
 
@@ -15,6 +16,7 @@
             }
         },
         "zh-hk": {
+            "name": "繁體中文(香港)",
             "fwtitle" : "",
             "strings":{
 
@@ -27,9 +29,10 @@
             }
         },
         "zh-cn": {
+            "name": "简体中文(简体)",
             "fwtitle" : "",
             "strings":{
-
+                
             },
             "titles":{
 

+ 160 - 7
src/web/desktop.system

@@ -86,6 +86,10 @@
         .showBackground {
             opacity: 1;
         }
+
+        /*
+            Desktop icons
+        */
         
         .icon-wrapper {
             height: 100%;
@@ -597,7 +601,7 @@
         }
 
         .qtwrapper.backgroundtask i{
-            margin-top: -1px;
+            margin-top: -5px;
             margin-right: 1px;
         }
 
@@ -657,6 +661,10 @@
             pointer-events: auto;
         }
 
+        body.whiteTheme .item:hover{
+            background-color: #ececec !important;
+        }
+
         @supports not (backdrop-filter: blur(2px)) {
             .notificationbar .cover{
                 /*
@@ -736,6 +744,9 @@
             right:4px;
             z-index:114;
             width:300px;
+            max-height: calc(100% - 100px);
+            overflow-y: auto;
+            scrollbar-width: thin;
             border-radius: 10px;
             border-top-right-radius: 0px;
             border-top-left-radius: 0px;
@@ -804,6 +815,10 @@
            color: var(--text_color);
         }
 
+        body.darkTheme #quickAccessPanel .ui.basic.button{
+            color: var(--text_color_secondary) !important;
+        }
+
         .item.module{
             cursor:pointer;
         }
@@ -855,6 +870,12 @@
             border: 0px;
         }
 
+        .alternativeAccount.item{
+            padding-top: 0.6em !important;
+            padding-bottom: 0.6em !important;
+            margin: 0;
+        }
+
         /* File operation progress tracker */
         #backgroundTaskPanel{
             background-color: var(--body_background) !important;
@@ -1187,7 +1208,7 @@
                     <img class="usericon" src="img/desktop/system_icon/user.svg">
                 </div>
                 <div class="content" style="padding-left: 1em;">
-                    <div id="username" class="header" style="font-weight: 500; font-size: 1.22em;">User</div>
+                    <div id="username" class="header" style="font-weight: 600; font-size: 1.22em;">User</div>
                     <div class="meta" style="margin-top: 0.15em;">
                         <div id="usergroups">@Users</div>
                     </div>
@@ -1198,6 +1219,18 @@
                     </div>
                 </div>
             </div>
+            <div class="ui divider"></div>
+            <div id="alternativeAccountList">
+               
+            </div>
+            <div class="ui divider" style="margin-left: 3em; margin-right: 3em;"></div>
+            <div class="item" style="padding-top: 6px; padding-bottom:6px; margin: 0px !important;" onclick="openSwitchAccountPanel(); hideToolPanel();">
+                <i class="ui user plus icon" style="margin-right: 0.6em;"></i> <span locale="account/switch/addAccount">Add another account</span>
+            </div>
+            <div id="signoutAllButton" style="padding-top: 6px; padding-bottom:6px; margin: 0px !important;" class="item" onclick="logoutAllAccounts();">
+                <i class="log out icon icon" style="margin-right: 0.6em;"></i> <span locale="account/switch/signoutAll">Sign-out all accounts</span>
+            </div>
+            <div class="ui divider"></div>
         </div>
         <div class="item" style="padding-bottom:12px;">
             <i class="volume up icon"></i> <span locale="quickAccess/sysvol">System Global Volume</span>
@@ -1318,7 +1351,7 @@
         var isTouchScreen = window.matchMedia("(any-pointer: coarse)").matches;
 
         //Check and prepare localization
-        if (applocale){
+        if (typeof(applocale) != "undefined"){
             //Applocale found. Do localization
             applocale.init("./SystemAO/locale/desktop.json", function(){
                 applocale.translate();
@@ -1355,6 +1388,9 @@
             setInterval(function() {
                 checkConnection();
             }, 15000);
+
+            //Activate all dropdowns
+            $(".dropdown").dropdown();
         }
 
         function initUploadCuttoffValues(){
@@ -1627,13 +1663,13 @@
             });
         }
 
-        function initDesktopUserInfo(){
+        function initDesktopUserInfo(callback=undefined){
             $.get("system/desktop/user", function(data){
                 if (data.error !== undefined){
                     alert(data.error);
                 }else{
                     userInfo = data;
-                   
+                    listAllStoredAccounts();
 
                     //Update the user tag
                     $("#username").text(userInfo.Username);
@@ -1641,6 +1677,8 @@
                     $("#usergroups").attr("title",userInfo.UserGroups.join(" / "));
                     if (data.UserIcon !== ""){
                         $(".usericon").attr("src",data.UserIcon);
+                    }else{
+                        $(".usericon").attr("src", "img/desktop/system_icon/user.svg")
                     }
 
                     if (data.IsAdmin == false){
@@ -1650,6 +1688,10 @@
                         //User is admin. Add admin icon
                         $("#username").append(`<i style="margin-left: 0.4em; color: #${desktopThemeColor}" class="small shield alternate icon themed text"></i>`);
                     }
+
+                    if (callback != undefined){
+                        callback();
+                    }
                 }
             });
         }
@@ -1811,10 +1853,10 @@
         }
 
         function hookLaunchMenuEvents(){
-            $(".groupType").on("click touchstart",function(){
+            $(".groupType").off("click touchstart").on("click touchstart",function(){
                 moduleTypeButtonClicked(this);
             });
-            $(".poweroption").on("click touchstart",function(){
+            $(".poweroption").off("click touchstart").on("click touchstart",function(){
                 logout();
             });
         }
@@ -3224,6 +3266,9 @@
                 //Localstorage has interval setting that is not NaN
                 backgroundCrossfadeInterval = parseInt(localStorage.getItem("ao/desktop/backgroundInterval")) * 1000;
             }
+            if (backgroundIntervalCounter != undefined){
+                clearInterval(backgroundIntervalCounter);
+            }
 
             backgroundIntervalCounter = setInterval(function() {
                 $("body").css("background-image", "none").css({
@@ -5098,6 +5143,16 @@
             hideAllContextMenus();
         }
 
+        function openSwitchAccountPanel(){
+            var uuid = newFloatWindow({
+                url: 'SystemAO/advance/switchAccount.html',
+                width: 470,
+                height: 680,
+                appicon: "SystemAO/desktop/img/account-switch.png",
+                title: "Switch Account"
+            });
+        }
+
         // ======================= CONTEXT MENU =============================
 
         window.addEventListener("contextmenu",
@@ -6834,6 +6889,8 @@
                 $.get("system/file_system/preference?key=file_explorer/theme", function(data){
                     if (data == "darkTheme"){
                         setDarkTheme();
+                    }else{
+                        setWhiteTheme();
                     }
                 });
             }else{
@@ -7198,6 +7255,102 @@
                 }
             });
         }
+
+        /*
+            Alternative account manager
+        */
+        function listAllStoredAccounts(){
+            $("#alternativeAccountList").empty();
+            //Request server side for the account pool
+            $.get("system/auth/u/list", function(data){
+                if (data.error != undefined){
+                    $("#alternativeAccountList").append(`<div style="padding: 0.4em; padding-left: 1em; padding-right: 1em;">
+                            <i class="ui green check circle icon"></i> ${applocale.getString("account/switch/noAlternative", "No other account stored on this browser")}
+                    </div>`);
+                    $("#signoutAllButton").addClass('disabled');
+                }else{
+                    if (data.length > 1){
+                        data.forEach(function(account){
+                            if (account.Username == userInfo.Username){
+                                //Skip
+                                return;
+                            }
+                            $.get("system/desktop/user?target=" + account.Username, function(data){
+                                let userIcon = data.UserIcon;
+                                if (userIcon == ""){
+                                    userIcon = "img/desktop/system_icon/user.svg"
+                                }
+                                $("#alternativeAccountList").append(`
+                                    <div class="alternativeAccount item ${account.IsExpired?"expired":""}" acname="${account.Username}" onclick="switchAccount(this);">
+                                        <div class="ui header">
+                                            <img class="usericon ui circular image" src="${userIcon}">
+                                            <div class="content" style="font-size: 95% !important;">
+                                                <span class="username">${account.Username}</span> ${(data.IsAdmin)?'<i style="margin-left: 0.4em; color: rgb(38, 50, 56);" title="Admin" class="small shield alternate icon themed text"></i>':""}
+                                                <div class="sub header usergroup">${!account.IsExpired?"<i class='ui green check circle icon' style='margin-right: 0px;'></i> " + applocale.getString("account/switch/sessionValid", "Session Valid"):"<i class='ui red times circle icon' style='margin-right: 0px;'></i> " + applocale.getString("account/switch/sessionExpired", "Session Expired")}</div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                `);
+                            });
+                        });
+                        $("#signoutAllButton").removeClass('disabled');
+                    }else{
+                        $("#signoutAllButton").addClass('disabled');
+                        $("#alternativeAccountList").append(`<div style="padding: 0.4em; padding-left: 1em; padding-right: 1em;">
+                            <i class="ui green check circle icon"></i> ${applocale.getString("account/switch/noAlternative", "No other account stored on this browser")}
+                        </div>`);
+                        return;
+                    }
+                }
+            })
+        }
+
+        function switchAccount(object){
+            let targetUsername = $(object).attr("acname");
+            if (targetUsername == undefined || targetUsername == ""){
+                console.log("Unable to load username from element")
+                return;
+            }
+
+            //Check if it is expired
+            if ($(object).hasClass("expired")){
+                openSwitchAccountPanel();
+                return;
+            }
+
+            $.ajax({
+                url: "system/auth/u/switch",
+                data: {
+                    "username": targetUsername,
+                },
+                success: function(data){
+                    if (data.error != undefined){
+                        alert(data.error);
+                    }else{
+                        initDesktop();
+                    }
+                }
+            })
+        }
+
+        function logoutAllAccounts(){
+            if (confirm(applocale.getString("account/switch/logout/confirm", "This will logout all other accounts from this browser. Confirm?"))){
+                $.ajax({
+                    url: "system/auth/u/logoutAll",
+                    success: function(data){
+                        if (data.error != undefined){
+                            alert(data.error);
+                        }else{
+                            //Reset the browser pool id
+                            localStorage.removeItem("ao_acc");
+                            listAllStoredAccounts();
+                            hideToolPanel();
+                        }
+                    }
+                })
+            }
+        }
+       
     </script>
 </body>
 

二进制
src/web/img/desktop/bg/default/0.jpg


二进制
src/web/img/desktop/bg/default/1.jpg


二进制
src/web/img/desktop/bg/default/3.jpg


二进制
src/web/img/desktop/bg/default/4.jpg


二进制
src/web/img/public/auth_bg.jpg


二进制
src/web/img/public/auth_bg.psd


二进制
src/web/img/public/auth_icon.png


二进制
src/web/img/public/auth_icon.psd


+ 4 - 4
src/web/login.system

@@ -72,21 +72,21 @@
     }
 
     .themecolor{
-        background-color: #485b73 !important;
+        background-color: #6eacfc !important;
         transition: background-color 0.1s;
     }
 
     .themecolor:hover{
-        background-color: #677c96 !important;
+        background-color: #63a7ff !important;
     }
 
     .subthemecolor{
-        background-color: #3e4f64 !important;
+        background-color: #6eacfc !important;
         transition: background-color 0.1s;
     }
 
     .subthemecolor:hover{
-        background-color: rgb(74, 88, 105) !important;
+        background-color: #61a6ff !important;
     }
 
     .loginbtn{