...
 
Commits (2)
/* Labels */
.title {
-fx-font-size: 2em;
-fx-alignment: center;
-fx-text-alignment: center;
}
.section {
-fx-font-size: 1.4em;
-fx-alignment: center;
-fx-text-alignment: center;
-fx-text-fill: #3E3E3E;
}
/* Side Menu */
#side-menu {
-fx-background-color: #FFFFFF;
-fx-effect: innershadow(three-pass-box, #00000040, 3, 0, -1, 0);
-fx-padding: 7px;
-fx-spacing: 7px;
}
\ No newline at end of file
......@@ -4,6 +4,7 @@ import errors.APIConfigError
import requests.RequestAuth
import utils.config.{APIConfig, UserConfig}
import errors.APIConfigError
import utils.AuthManager
/**
* A simple requests wrapper to send requests to phpipam server.
......@@ -11,10 +12,10 @@ import errors.APIConfigError
object Client {
/* Base URL */
private val baseURL: String = UserConfig.getApiUrl.getOrElse(
private def baseURL: String = UserConfig.getApiUrl.getOrElse(
throw new APIConfigError("Server base URL is missing")
)
private val appName: String = UserConfig.getAppName.getOrElse(
private def appName: String = UserConfig.getAppName.getOrElse(
throw new APIConfigError("App name is missing")
)
......@@ -24,9 +25,6 @@ object Client {
private val subnetEndpoint: String = APIConfig.getSubnetEndpoint
private val addressEndpoint: String = APIConfig.getAddressEndpoint
/* Various options */
private val verifySsl: Boolean = UserConfig.getVerifySsl
/**
* Send an authentication requests and retry `retry` times if it fails.
*
......@@ -43,7 +41,7 @@ object Client {
val r = requests.post(
f"$baseURL/$appName/$loginEndpoint",
auth = auth,
verifySslCerts = verifySsl
verifySslCerts = UserConfig.getVerifySsl
)
(r.statusCode, r.text)
} catch {
......@@ -62,6 +60,7 @@ object Client {
* @param method The method to use (see `requestMethod` for avilable methods)
* @param params An array of parameters to send with the quests
* @param retry Remaining tries before throwing an exception
* @param token The auth token
* @return A tuple containing the status code and the json text returned by
* the API.
*/
......@@ -69,8 +68,9 @@ object Client {
private def sendRequest(
endpoint: String,
method: String,
params: List[(String, String)] = List[(String, String)](),
retry: Int = 5
token: String,
params: List[(String, String)],
retry: Int
): (Int, String) = {
val requestMethod = method match {
......@@ -82,7 +82,7 @@ object Client {
case _ => throw new APIConfigError(f"Invalid method: $method")
}
val header = Map("token" -> UserConfig.getToken.getOrElse(""))
val header = Map("token" -> token)
// Send the request and retry if the request failed and retry != 0
try {
......@@ -90,7 +90,7 @@ object Client {
endpoint,
headers = header,
params = params,
verifySslCerts = verifySsl
verifySslCerts = UserConfig.getVerifySsl
)
// RETURN VALUES
......@@ -100,11 +100,51 @@ object Client {
case e:Throwable =>
if (retry > 0) {
Thread.sleep(500)
sendRequest(endpoint,method,params,retry-1)
sendRequest(endpoint,method,token,params,retry-1)
} else throw e
}
}
/**
* Utility function to call `sendrequest` without token.
* The token is retrieved from the AuthManager.
*/
private def sendRequest(
endpoint: String,
method: String,
params: List[(String, String)] = List[(String, String)](),
retry: Int = 5
): (Int, String) = {
val token = AuthManager.refreshAndGetToken()
sendRequest(endpoint,method,token,params,retry)
}
/**** AUTH ****/
/**
* Send a request to check if an auth token is still valid.
*
* @param token The token to validate
* @return
*/
def validateToken(token: String):(Int, String) = {
val endpoint = f"$baseURL/$appName/$loginEndpoint"
val params = List[(String, String)]()
sendRequest(endpoint, method="GET", token, params, retry = 1)
}
/**
* Send a request to refresh a valid auth token.
*
* @param token The valid auth token to refresh
* @return
*/
def refreshToken(token: String): (Int, String) = {
val endpoint = f"$baseURL/$appName/$loginEndpoint"
val params = List[(String, String)]()
sendRequest(endpoint, method="PATCH", token, params, retry = 1)
}
/**** SECTIONS ****/
/**
......
package gui
import errors.NotLoggedInError
import gui.dialog.ErrorDialog
import gui.panel.MainPanel
import scalafx.application.JFXApp.PrimaryStage
import scalafx.application.Platform
import scalafx.scene.Scene
import scalafx.scene.image.Image
import scalafx.scene.layout.BorderPane
import utils.config.UserConfig
/**
......@@ -27,6 +33,45 @@ object MainStage extends PrimaryStage {
/* Window icon */
icons.add(new Image("/images/icon.png"))
/**
* Function to initialize the GUI.
*/
def init(): Unit = {
Thread.setDefaultUncaughtExceptionHandler(showError)
MainPanel.displayIpamPanel()
}
/* Main Scene */
scene = new Scene() {
stylesheets.add(getClass.getResource("/sipam.css").toExternalForm)
root = new BorderPane() {
top = TopMenu
center = MainPanel
left = SideMenu
}
init()
}
/*
* Default error handler
*
* NotLoggedInError -> Display a login panel
* Anything else -> Display the error in a nice popup
*
* */
private def showError(thread: Thread, error: Throwable): Unit = {
error match {
case _:NotLoggedInError => Platform.runLater(MainPanel.displayLoginPanel())
case e => Platform.runLater({
new ErrorDialog(
f"An unexpected error occurred : \n $e",
Some(e.getStackTrace.mkString("\n"))
)
})
}
}
/* Resize events */
width.onChange { (_, _, newValue) =>
......
package gui
import scalafx.Includes._
import gui.button.SideMenuButton
import scalafx.geometry.Pos.TopCenter
import scalafx.scene.AccessibleRole.ImageView
import scalafx.scene.Node
import scalafx.scene.image.{Image, ImageView}
import scalafx.scene.layout.VBox
/**
* The Side Menu for the main window.
*/
object SideMenu extends VBox {
/* STYLE */
id = "side-menu"
private val logo = new ImageView("/images/icon.png"){
preserveRatio = true
alignment = TopCenter
fitWidth = 100
}
private val viewIpamItem = new SideMenuButton("IPAM")
private val configItem = new SideMenuButton("Configuration")
children = List[Node](
logo,
viewIpamItem,
configItem
)
}
package gui
import scalafx.scene.control.{Menu, MenuBar, MenuItem}
import scalafx.scene.input.{KeyCode, KeyCodeCombination, KeyCombination}
/**
* The Top Menu for the main window.
*/
object TopMenu extends MenuBar{
/* File Category */
private val files: Menu = new Menu("File") {
private val open = new MenuItem("Open"){
accelerator = new KeyCodeCombination(KeyCode.O, KeyCombination.ControlDown)
}
private val save = new MenuItem("Save"){
accelerator = new KeyCodeCombination(KeyCode.S, KeyCombination.ControlDown)
}
/* Closes the window */
private val exit = new MenuItem("Exit"){
accelerator = new KeyCodeCombination(KeyCode.Q, KeyCombination.ControlDown)
onAction = _ => MainStage.close()
}
items = List[MenuItem](open, save, exit)
}
/* Edit Category */
private val edit = new Menu("Edit")
/* Help Category */
private val help = new Menu("Help")
menus = List[Menu](
files,
edit,
help
)
}
package gui.button
import scalafx.scene.control.Button
/**
* A simple Button for the side menu. Every button has the same size.
*
* @param label The label of the button
*/
class SideMenuButton(label: String) extends Button(label){
maxWidth = Double.MaxValue
}
package gui.dialog
import gui.MainStage
import scalafx.scene.control.{Alert, Label}
import scalafx.scene.control.Alert.AlertType
/**
* An error dialog with an expendable section.
*
* @param shortMsg The main message
* @param msg The message displayed in the expandable section
*/
class ErrorDialog(shortMsg: String, msg: Option[String] = None)
extends Alert(AlertType.Error){
initOwner(MainStage)
title = "ERROR"
headerText = "Error"
contentText = shortMsg
if(msg.isDefined) dialogPane().setExpandableContent(new Label(msg.get))
showAndWait()
}
package gui.label
import scalafx.scene.control.Label
class Section(title: String) extends Label(title){
styleClass.add("section")
maxWidth = Double.MaxValue
}
package gui.label
import scalafx.scene.control.Label
class Title(title: String) extends Label(title){
styleClass.add("title")
maxWidth = Double.MaxValue
}
package gui.panel
import scalafx.Includes._
import com.panemu.tiwulfx.control.{DetachableTab, DetachableTabPane}
import scalafx.scene.layout.Priority.Always
import scalafx.scene.layout.VBox
class IpamPanel extends VBox with LoginRequired {
def initialize(): Unit = {
println("ok")
}
hgrow = Always
children = new DetachableTabPane(){
//getTabs.add(new DetachableTab("TEST"))
}
}
package gui.panel
import gui.dialog.ErrorDialog
import gui.label.{Section, Title}
import scalafx.application.Platform
import scalafx.geometry.Insets
import scalafx.geometry.Pos.{BaselineRight, TopCenter}
import scalafx.scene.control._
import scalafx.scene.layout.GridPane
import utils.AuthManager
import utils.config.UserConfig
/**
* The login panel
*/
class LoginPanel extends GridPane{
private val loader = new ProgressIndicator()
/* Title */
private val title = new Title("Login")
/* API URL */
private val apiUrlLabel = new Label("API URL ")
private val apiUrlField = new TextField(){
text = UserConfig.getApiUrl.getOrElse("")
promptText = "API URL"
}
/* Verify SSL */
private val verifySslLabel = new Label("Check SSL certificate ")
private val verifySslcheckBox = new CheckBox(){
selected = UserConfig.getVerifySsl
}
/* API APP Name */
private val appNameLabel = new Label("API App name ")
private val appNameField = new TextField(){
text = UserConfig.getAppName.getOrElse("")
promptText = "App Name"
}
/* Credential section title */
private val credentialSection = new Section("Credentials")
/* Credentials */
private val usernameLabel = new Label("Username ")
private val usernameField = new TextField(){
promptText = "Username"
}
private val passwordLabel = new Label("Password ")
private val passwordField = new PasswordField(){
promptText = "Password"
}
private val loginButton = new Button(){
text = "Login"
alignmentInParent = BaselineRight
onMouseClicked = _ => {
if (checkUrl()) login()
}
}
/* Style */
alignment = TopCenter
padding = Insets(20, 100, 10, 10)
vgap = 20
hgap = 20
/* Add elements to the grid */
add(title, 0, 0, 4, 1)
add(apiUrlLabel, 0, 1)
add(apiUrlField, 1, 1)
add(verifySslLabel, 2,1)
add(verifySslcheckBox, 3,1)
add(appNameLabel, 0,2)
add(appNameField, 1,2)
add(credentialSection, 0, 3, 4,1)
add(usernameLabel, 0,4)
add(usernameField, 1,4)
add(passwordLabel, 0,5)
add(passwordField, 1,5)
add(loginButton,1,6)
/**
* Check if the url provided is somewhat valid
*
* @return Whether the url is valid or not
*/
private def checkUrl(): Boolean = {
val httpr = "^(https?://)".r
val url = apiUrlField.text.value
val errorMsg =
if (httpr.findFirstMatchIn(url).isEmpty)
Some(s"${appNameLabel.text.value.strip()} should inlude the protocol.")
else None
/* Display the error */
if (errorMsg.isDefined) {
new ErrorDialog(errorMsg.get)
false
} else {
true
}
}
private def login(): 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()
}
}
package gui.panel
import errors.NotLoggedInError
import scalafx.application.Platform
import scalafx.scene.layout.Pane
import utils.AuthManager
trait LoginRequired extends Pane {
def initialize(): Unit
new Thread {
override def run(): Unit = {
if (AuthManager.isAuthenticated) Platform.runLater(initialize())
else throw new NotLoggedInError
}
}.start()
}
package gui.panel
import scalafx.scene.Node
import scalafx.scene.layout.AnchorPane
/**
* The Main Panel.
* It exposes methods to display other panel in the center of the main window.
*
*/
object MainPanel extends AnchorPane {
/* Display a node */
private def displayNode(node: Node): Unit = {
children = node
AnchorPane.setLeftAnchor(node, 0)
AnchorPane.setRightAnchor(node, 0)
AnchorPane.setTopAnchor(node, 0)
AnchorPane.setBottomAnchor(node, 0)
}
/* Display a specific Ipam Panel */
def displayIpamPanel(): Unit = {
displayNode(new IpamPanel)
}
def displayLoginPanel(): Unit = {
displayNode(new LoginPanel)
}
}
package gui.panel
import api.objects.Subnet
import scalafx.scene.layout.GridPane
class SubnetPanel(id: String) extends GridPane {
private val subnet = Subnet.getSubnet(id)
}
package utils
import java.time.{Duration, LocalDateTime}
import api.Client
import api.objects.AuthToken
import errors.NotLoggedInError
import requests.{InvalidCertException, RequestFailedException, UnknownHostException}
import utils.config.UserConfig
/**
* An object managing sessions with the API.
*/
object AuthManager {
private var lastRefresh = LocalDateTime.now() // TODO: TEST ME
/**
* Check if the user is authenticated.
* @return true if the user is authenticated, false otherwise.
*/
def isAuthenticated: Boolean = {
val token = UserConfig.getToken
token match {
// There is no token in the config file
case None => false
// There is a token in the config file
case Some(t) =>
try {
Client.validateToken(t)
true // Token is valid
}
catch { // The request failed
case e:RequestFailedException => false
case _:UnknownHostException => false
case _:InvalidCertException => false
case e:Throwable => throw e // unexpected failure, throw it
}
}
}
/**
* Send an authentication request and store the result.
*
* @param user The username
* @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]) = {
try {
val authToken = AuthToken.getAuthToken(user, passwd)
UserConfig.setToken(authToken.token)
(true, None)
} 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 e: Throwable => throw e
}
}
/**
* The method should be **the only way to retrieve the auth token**.
* It refreshes the token if needed, and throw an error if the user is not
* logged in.
*
* @return A valid auth token
*/
def refreshAndGetToken(): String = {
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
}
}
......@@ -35,7 +35,7 @@ object UserConfig {
Config()
}
def getToken: Option[String] = {
private[utils] def getToken: Option[String] = {
conf.token
}
......