...
 
Commits (2)
/* Default component theme */
.button:focused, .text-field:focused, .check-box:focused{
-fx-faint-focus-color: transparent;
-fx-focus-color: #345169;
}
.text-input {
-fx-highlight-fill: #345169;
}
.progress-indicator {
-fx-progress-color: #345169;
}
.menu-bar .menu-button:hover, .menu-bar .menu-button:focused, .menu-bar .menu-button:showing {
-fx-background-color: #345169;
}
.menu-item:hover, .menu-item:focused{
-fx-background-color: #345169;
}
/* Labels */
.title {
-fx-font-size: 2em;
......@@ -17,5 +40,5 @@
-fx-background-color: #FFFFFF;
-fx-effect: innershadow(three-pass-box, #00000040, 3, 0, -1, 0);
-fx-padding: 7px;
-fx-spacing: 7px;
-fx-spacing: 12px;
}
\ No newline at end of file
......@@ -25,6 +25,10 @@ object Client {
private val subnetEndpoint: String = APIConfig.getSubnetEndpoint
private val addressEndpoint: String = APIConfig.getAddressEndpoint
/* Timeouts */
private val readTimeout = 10000
private val connectTimeout = 2000
/**
* Send an authentication requests and retry `retry` times if it fails.
*
......@@ -35,13 +39,15 @@ object Client {
* the API.
*/
@scala.annotation.tailrec
def requestAuthToken(user: String, pass: String, retry: Int = 5): (Int, String) = {
def requestAuthToken(user: String, pass: String, retry: Int = 3): (Int, String) = {
val auth = RequestAuth.implicitBasic(user, pass)
try {
val r = requests.post(
f"$baseURL/$appName/$loginEndpoint",
auth = auth,
verifySslCerts = UserConfig.getVerifySsl
verifySslCerts = UserConfig.getVerifySsl,
readTimeout = readTimeout,
connectTimeout = connectTimeout
)
(r.statusCode, r.text)
} catch {
......@@ -90,7 +96,9 @@ object Client {
endpoint,
headers = header,
params = params,
verifySslCerts = UserConfig.getVerifySsl
verifySslCerts = UserConfig.getVerifySsl,
readTimeout = readTimeout,
connectTimeout = connectTimeout
)
// RETURN VALUES
......@@ -113,7 +121,7 @@ object Client {
endpoint: String,
method: String,
params: List[(String, String)] = List[(String, String)](),
retry: Int = 5
retry: Int = 3
): (Int, String) = {
val token = AuthManager.refreshAndGetToken()
sendRequest(endpoint,method,token,params,retry)
......
......@@ -37,7 +37,7 @@ object MainStage extends PrimaryStage {
* Function to initialize the GUI.
*/
def init(): Unit = {
Thread.setDefaultUncaughtExceptionHandler(showError)
Thread.setDefaultUncaughtExceptionHandler(displayError)
MainPanel.displayIpamPanel()
}
......@@ -60,9 +60,10 @@ object MainStage extends PrimaryStage {
*
* */
private def showError(thread: Thread, error: Throwable): Unit = {
private def displayError(thread: Thread, error: Throwable): Unit = {
error match {
case _:NotLoggedInError => Platform.runLater(MainPanel.displayLoginPanel())
case _:NotLoggedInError =>
Platform.runLater(MainPanel.displayLoginPanel())
case e => Platform.runLater({
new ErrorDialog(
f"An unexpected error occurred : \n $e",
......@@ -85,5 +86,4 @@ object MainStage extends PrimaryStage {
maximized.onChange {(_, _, _) =>
UserConfig.setMaximized(maximized())
}
}
......@@ -23,10 +23,12 @@ object SideMenu extends VBox {
}
private val viewIpamItem = new SideMenuButton("IPAM")
private val configItem = new SideMenuButton("Configuration")
private val logoutItem = new SideMenuButton("Log Out")
children = List[Node](
logo,
viewIpamItem,
configItem
configItem,
logoutItem
)
}
......@@ -2,17 +2,16 @@ package gui.panel
import scalafx.Includes._
import com.panemu.tiwulfx.control.{DetachableTab, DetachableTabPane}
import scalafx.scene.control.ProgressIndicator
import scalafx.scene.layout.Priority.Always
import scalafx.scene.layout.VBox
import scalafx.scene.layout.{BorderPane, VBox}
class IpamPanel extends VBox with LoginRequired {
class IpamPanel extends BorderPane with LoginRequired {
def initialize(): Unit = {
println("ok")
}
hgrow = Always
children = new DetachableTabPane(){
//getTabs.add(new DetachableTab("TEST"))
}
center = new ProgressIndicator()
}
......@@ -2,18 +2,27 @@ package gui.panel
import gui.dialog.ErrorDialog
import gui.label.{Section, Title}
import scalafx.Includes._
import scalafx.application.Platform
import scalafx.geometry.Insets
import scalafx.geometry.Pos.{BaselineRight, TopCenter}
import scalafx.scene.control._
import scalafx.scene.input.{KeyCode, KeyEvent}
import scalafx.scene.layout.GridPane
import scala.concurrent.ExecutionContext.Implicits.global
import utils.AuthManager
import utils.config.UserConfig
import scala.util.{Failure, Success}
/**
* The login panel
*
* @param callback: The action to undergo after a successful login.
*/
class LoginPanel extends GridPane{
class LoginPanel(callback: () => Unit) extends GridPane{
private val loader = new ProgressIndicator()
......@@ -48,17 +57,18 @@ class LoginPanel extends GridPane{
private val usernameField = new TextField(){
promptText = "Username"
}
private val passwordLabel = new Label("Password ")
private val passwordField = new PasswordField(){
promptText = "Password"
onKeyPressed = (e:KeyEvent) => if(e.code == KeyCode.Enter) startLogin()
}
private val loginButton = new Button(){
text = "Login"
alignmentInParent = BaselineRight
onMouseClicked = _ => {
if (checkUrl()) login()
}
/* Start the authentication process when clicked */
onMouseClicked = _ => startLogin()
}
/* Style */
......@@ -86,7 +96,31 @@ class LoginPanel extends GridPane{
add(passwordLabel, 0,5)
add(passwordField, 1,5)
add(loginButton,1,6)
/* Wrap button positioning in a small function for further reuse */
private def addLoginButton(): Unit = add(loginButton,1,6)
addLoginButton()
/* Start login sequence */
private def startLogin(): Unit = {
if (checkUrl()) {
toggleLoader()
saveNewConfig()
// Async task t avoid freezing the GUI
val resultFuture = AuthManager.authenticate(
usernameField.text.value,
passwordField.text.value
)
resultFuture onComplete {
case Success((succeeded, msg)) =>
Platform.runLater(postLogin(succeeded, msg))
case Failure(e) => Platform.runLater({
toggleLoader(hide = true)
new ErrorDialog(e.getMessage, Some(e.getStackTrace.mkString("\n")))
})
}
}
}
/**
* Check if the url provided is somewhat valid
......@@ -98,8 +132,10 @@ class LoginPanel extends GridPane{
val url = apiUrlField.text.value
val errorMsg =
if (httpr.findFirstMatchIn(url).isEmpty)
Some(s"${appNameLabel.text.value.strip()} should inlude the protocol.")
if (httpr.findFirstMatchIn(url).isEmpty) {
val text = "should include a protocol such as http or https."
Some(s"${appNameLabel.text.value.strip()} $text")
}
else None
/* Display the error */
......@@ -111,30 +147,46 @@ class LoginPanel extends GridPane{
}
}
private def login(): Unit = {
/**
* Save the new configuration
*/
private def saveNewConfig(): Unit = {
/* Save the new configuration */
UserConfig.setApiUrl(apiUrlField.text.value.stripSuffix("/"))
UserConfig.setAppName(appNameField.text.value)
UserConfig.setVerifySsl(verifySslcheckBox.selected.value)
}
/* Add a loading animation */
add(loader, 2, 4,2,2)
/* Start the authentication process */
new Thread{
override def run() {
AuthManager.authenticate(usernameField.text.value,passwordField.text.value)
match {
case (true, None) => println("It works !")
case (false, Some(msg)) =>
Platform.runLater(new ErrorDialog(msg))
case _ =>
val msg = "This code shouldn't run."
Platform.runLater(new ErrorDialog(msg))
}
/* Remove the loading animation */
Platform.runLater(children.remove(loader))
}
}.start()
/**
* Call the login callback such as displaying another panel if the login
* process succeed.
* If the process failed, an error is displayed.
*
* @param success Whether the login was successful
* @param msg An optional error message
*/
def postLogin(success: Boolean, msg: Option[String]): Unit ={
toggleLoader(hide = true)
val defaultMsg = "No error message returned, this is not supposed to happen"
if (success)
callback()
else
new ErrorDialog(msg.getOrElse(defaultMsg))
}
/**
* Display or add the loading indicator.
* @param hide Whether to hide or display the loader
*/
def toggleLoader(hide: Boolean = false): Unit ={
if (hide) children.remove(loader)
else try{
add(loader, 2, 4,2,2)
} catch {
case e:IllegalArgumentException => // Already displayed
case e:Throwable => throw e
}
}
}
package gui.panel
import errors.NotLoggedInError
import gui.dialog.ErrorDialog
import scalafx.application.Platform
import scalafx.scene.layout.Pane
import utils.AuthManager
trait LoginRequired extends Pane {
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
/**
* A simple trait which checks if the user is authenticated and throw an
* error if not.
*/
trait LoginRequired {
/*
* Callback to implement in order to display the panel after checking the
* authentication.
*/
def initialize(): Unit
new Thread {
override def run(): Unit = {
if (AuthManager.isAuthenticated) Platform.runLater(initialize())
else throw new NotLoggedInError
}
}.start()
// Async task to check user authentication.
AuthManager.isAuthenticated onComplete {
case Success(true) => Platform.runLater(initialize())
case Success(false) => Platform.runLater(MainPanel.displayLoginPanel())
case Failure(e) => Platform.runLater(
new ErrorDialog(e.getMessage, Some(e.getStackTrace.mkString("\n")))
)
}
}
......@@ -24,8 +24,8 @@ object MainPanel extends AnchorPane {
displayNode(new IpamPanel)
}
def displayLoginPanel(): Unit = {
displayNode(new LoginPanel)
def displayLoginPanel(callback: () => Unit = displayIpamPanel): Unit = {
displayNode(new LoginPanel(callback))
}
}
......@@ -8,18 +8,22 @@ import errors.NotLoggedInError
import requests.{InvalidCertException, RequestFailedException, UnknownHostException}
import utils.config.UserConfig
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import scala.concurrent.{Await, Future}
/**
* An object managing sessions with the API.
*/
object AuthManager {
private var lastRefresh = LocalDateTime.now() // TODO: TEST ME
private var lastRefresh = LocalDateTime.now()
/**
* Check if the user is authenticated.
* @return true if the user is authenticated, false otherwise.
*/
def isAuthenticated: Boolean = {
def isAuthenticated: Future[Boolean] = Future {
val token = UserConfig.getToken
token match {
// There is no token in the config file
......@@ -31,7 +35,7 @@ object AuthManager {
true // Token is valid
}
catch { // The request failed
case e:RequestFailedException => false
case _:RequestFailedException => false
case _:UnknownHostException => false
case _:InvalidCertException => false
case e:Throwable => throw e // unexpected failure, throw it
......@@ -46,7 +50,8 @@ object AuthManager {
* @param passwd The password
* @return Whether the authentication succeeded and a message if it didn't.
*/
def authenticate(user: String, passwd: String): (Boolean, Option[String]) = {
def authenticate(user: String, passwd: String):
Future[(Boolean, Option[String])] = Future {
try {
val authToken = AuthToken.getAuthToken(user, passwd)
UserConfig.setToken(authToken.token)
......@@ -54,7 +59,7 @@ object AuthManager {
} catch {
case e:RequestFailedException => (false, Some(e.response.text))
case e:UnknownHostException => (false, Some(s"Host ${e.host} not found."))
case e:InvalidCertException => (false, Some(s"SSL verification failed"))
case _:InvalidCertException => (false, Some(s"SSL verification failed"))
case e: Throwable => throw e
}
}
......@@ -67,19 +72,15 @@ object AuthManager {
* @return A valid auth token
*/
def refreshAndGetToken(): String = {
val token = UserConfig.getToken.getOrElse(throw new NotLoggedInError)
val delta = Duration.between(lastRefresh,LocalDateTime.now)
var token = UserConfig.getToken.getOrElse(throw new NotLoggedInError)
// validate token if it's more than 60 minutes old
if (delta.toMinutes > 60) {
if(isAuthenticated) {
// refresh token if it's valid
Client.refreshToken(token)
// Save the new delta
lastRefresh = LocalDateTime.now
} else {
throw new NotLoggedInError
}
}
token
if (Await.result(isAuthenticated, 10.seconds)) {
Client.refreshToken(token)
lastRefresh = LocalDateTime.now
token
} else throw new NotLoggedInError
} else token
}
}