瀏覽代碼

Added experimental account switching interface

Toby Chui 2 年之前
父節點
當前提交
8f04e745ad
共有 9 個文件被更改,包括 737 次插入43 次删除
  1. 17 0
      auth.go
  2. 26 2
      desktop.go
  3. 1 13
      mediaServer.go
  4. 319 0
      mod/auth/accountSwitch.go
  5. 33 25
      mod/auth/auth.go
  6. 1 1
      mod/user/useropr.go
  7. 294 0
      web/SystemAO/advance/switchAccount.html
  8. 2 2
      web/SystemAO/locale/desktop.json
  9. 44 0
      web/desktop.system

+ 17 - 0
auth.go

@@ -105,4 +105,21 @@ func AuthSettingsInit() {
 
 	//Register nightly task for clearup all user retry counter
 	nightlyManager.RegisterNightlyTask(authAgent.ExpDelayHandler.ResetAllUserRetryCounter)
+
+	/*
+		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
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 - 13
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) {

+ 319 - 0
mod/auth/accountSwitch.go

@@ -0,0 +1,319 @@
+package auth
+
+import (
+	"encoding/json"
+	"errors"
+	"net/http"
+	"time"
+
+	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
+	Accounts []*SwitchableAccount //Accounts that is cross switchable in this pool
+	parent   *SwitchableAccountPoolManager
+}
+
+type SwitchableAccountPoolManager struct {
+	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) *SwitchableAccountPoolManager {
+	sysdb.NewTable("auth_acswitch")
+	thisManager := SwitchableAccountPoolManager{
+		Database:   sysdb,
+		ExpireTime: 604800,
+		authAgent:  parent,
+	}
+	return &thisManager
+}
+
+// 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
+	}
+
+	poolid, err := utils.GetPara(r, "pid")
+	if err != nil {
+		utils.SendErrorResponse(w, "invalid pool id given")
+		return
+	}
+
+	//Check pool exists
+	targetPool, err := m.GetPoolByID(poolid)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	//Check if the user can access this pool
+	if !targetPool.IsAccessibleBy(currentUsername) {
+		utils.SendErrorResponse(w, "permission denied")
+		return
+	}
+
+	//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))
+}
+
+// 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
+	}
+	poolid, err := utils.PostPara(r, "pid")
+	if err != nil {
+		utils.SendErrorResponse(w, "invalid pool id given")
+	}
+
+	//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()
+
+	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
+	}
+	poolid, err := utils.PostPara(r, "pid")
+	if err != nil {
+		//No pool is given. Generate a pool for this request
+		poolid = uuid.NewV4().String()
+		newPool := SwitchableAccountsPool{
+			UUID: poolid,
+			Accounts: []*SwitchableAccount{
+				{
+					Username:   previousUserName,
+					LastSwitch: time.Now().Unix(),
+				},
+			},
+			parent: m,
+		}
+
+		newPool.Save()
+	}
+
+	//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 := new(SwitchableAccountsPool)
+		err = json.Unmarshal(keypairs[1], &thisPool)
+		if err == nil {
+			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
+}
+
+/*
+	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)
+	}
+}
+
+// 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)
+}

+ 33 - 25
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)
+	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)
 
@@ -310,7 +318,7 @@ 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 != "" {
@@ -337,7 +345,7 @@ func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
 	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 +357,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 +415,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 +425,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
@@ -472,7 +480,7 @@ func (a *AuthAgent) UnregisterUser(username string) error {
 	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 +497,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 +510,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 +520,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,7 +545,7 @@ 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)
@@ -553,7 +561,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))

+ 1 - 1
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)

+ 294 - 0
web/SystemAO/advance/switchAccount.html

@@ -0,0 +1,294 @@
+<!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>
+    <style>
+        body{
+            background-color: rgb(240, 240, 240);
+        }
+
+        .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;
+        }
+    </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>Current account</p>
+            <div class="ui small basic right floated button" onclick="logout();">
+                <i class="log out icon"></i> <span locale="quickAccess/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>Saved accounts on this browser</p>
+            <div id="alternativeAccountList">
+
+            </div>
+            
+            
+            <div style="margin-top: 1em !important;">
+                <div class="ui fluid small black basic button" onclick="logoutAllAccounts();"><i class="log out icon icon"></i> Sign-out all accounts</div>
+            </div>
+            <div class="ui divider"></div>
+                <p>Sign-in to new account</p>
+            <form class="ui form" onsubmit="handleFormSubmit(event, this);">
+                <div class="field">
+                    <label>Username</label>
+                    <input id="username" type="text" name="username" value="">
+                </div>
+                <div class="field">
+                    <label>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> Login</button>
+            </form>
+            <div id="restoreSessionMessage" class="ui blue inverted segment" style="display:none;">
+                <span>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 = {};
+        let browserAccountPoolUUID = localStorage.getItem("ao_acc");
+
+        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,
+                    pid: (browserAccountPoolUUID!=undefined)?browserAccountPoolUUID:"",
+                },
+                success: function(data){
+                    if (data.error != undefined){
+                        $("#errtext").text(data.error);
+                        $("#errmsg").show();
+                    }else{
+                        //The returned value is the pool id. Save it to localstorage
+                        console.log("Setting browser switch pool id to " + data)
+                        localStorage.setItem("ao_acc", data)
+                        browserAccountPoolUUID = data;
+                        //Refresh the page
+                        $("#errmsg").hide();
+                        initCurrentAccountInfo(function(){
+                            listAllStoredAccounts();
+                        });
+                    }
+                    $("#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,
+                    "pid": browserAccountPoolUUID,
+                },
+                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("This will logout all other accounts from this browser. Confirm?")){
+                $.ajax({
+                    url: "/system/auth/u/logoutAll",
+                    data: {pid: browserAccountPoolUUID},
+                    success: function(data){
+                        if (data.error != undefined){
+                            showError(data.error);
+                        }else{
+                            //Reset the browser pool id
+                            browserAccountPoolUUID = "";
+                            localStorage.removeItem("ao_acc");
+                            hideError();
+                            listAllStoredAccounts();
+                        }
+                    }
+                })
+            }
+        }
+
+        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();
+            if (browserAccountPoolUUID == undefined){
+                 //Empty or no stored accounts
+                 $("#alternativeAccountList").append(`<div class="ui message">
+                    <i class="ui green check circle icon"></i> No other account stored on this browser
+                </div>`);
+                return;
+            }else{
+                //Request server side for the account pool
+                $.get("../../system/auth/u/list?pid=" + browserAccountPoolUUID, function(data){
+                    if (data.error != undefined){
+                        alert(data.error);
+                    }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);" 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> Session Valid":"<i class='ui red times circle icon' style='margin-right: 0px;'></i> Session Expired"}</div>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    `);
+                                });
+                            });
+                        }else{
+                            $("#alternativeAccountList").append(`<div class="ui message">
+                                <i class="ui green check circle icon"></i> 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();
+                }
+                
+            });
+        }
+
+        initCurrentAccountInfo(function(){
+            listAllStoredAccounts();
+        });
+
+        function logout() {
+            loggingOut = true;
+            if (confirm("Exiting Session. Confirm?")){
+                $.get("../../system/auth/logout", function() {
+                    window.location.href = "./";
+                });
+            }
+            hideAllContextMenus();
+        }
+    </script>
+    </body>
+</html>

+ 2 - 2
web/SystemAO/locale/desktop.json

@@ -391,10 +391,10 @@
                 
                 "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.",
     
                 "upload/message/uploading": "Uploading...",
                 "":""

+ 44 - 0
web/desktop.system

@@ -86,6 +86,10 @@
         .showBackground {
             opacity: 1;
         }
+
+        /*
+            Desktop icons
+        */
         
         .icon-wrapper {
             height: 100%;
@@ -1198,6 +1202,33 @@
                     </div>
                 </div>
             </div>
+            <div class="ui divider"></div>
+            <div class="item" style="padding-left: 8px;">
+                <div class="ui small items" style="margin-bottom: 0; width: 100%;">
+                    <div class="alternativeUsableAccount item"  style="padding-left: 0px;">
+                        <div class="ui mini image">
+                            <img class="accountIcon" src="img/desktop/system_icon/user.svg">
+                        </div>
+                        <div class="content" style="padding-left: 1em;">
+                            <div class="header accountName" style="font-weight: 500; font-size: 1em;">User</div>
+                            <div class="meta" style="margin-top: 0.15em;">
+                                <div class="accountGroups">@Users</div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="item">
+                <div class="ui small items" style="margin-bottom: 0; width: 100%;">
+                    <div class="item" style="margin-bottom: 0px !important;">
+                        <div class="ui fluid small basic button" onclick="openSwitchAccountPanel(); hideToolPanel();"><i class="ui user plus icon"></i> Add another account</div>
+                    </div>
+                    <div class="item" style="margin-top: 0px !important;">
+                        <div class="ui fluid small black basic button"><i class="log out icon icon"></i> Sign-out all accounts</div>
+                    </div>
+                </div>
+            </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>
@@ -1355,6 +1386,9 @@
             setInterval(function() {
                 checkConnection();
             }, 15000);
+
+            //Activate all dropdowns
+            $(".dropdown").dropdown();
         }
 
         function initUploadCuttoffValues(){
@@ -5098,6 +5132,16 @@
             hideAllContextMenus();
         }
 
+        function openSwitchAccountPanel(){
+            var uuid = newFloatWindow({
+                url: 'SystemAO/advance/switchAccount.html',
+                width: 470,
+                height: 680,
+                appicon: "",
+                title: "Switch Account"
+            });
+        }
+
         // ======================= CONTEXT MENU =============================
 
         window.addEventListener("contextmenu",