add toast messages

This commit is contained in:
Zhi Wang 2022-01-21 17:53:23 -05:00
parent 481e5473aa
commit 033d9b3f14
5 changed files with 96 additions and 65 deletions

View File

@ -14,8 +14,19 @@
</head>
<body class="text-center">
<div class="toast bg-primary text-white border-0" role="alert" aria-live="assertive" aria-atomic="true" id="authMsg"
style="position: absolute;top: 0px; right: 10px; z-index:1;">
<div class="d-flex">
<div class="toast-body">
{{.msg}}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
aria-label="Close"></button>
</div>
</div>
<main class="form-signin">
<form action="/login" method="post">
<form action="/login" method="post">
<img class="mb-4" src="/assets/img/keyboard.svg" alt="" width="64">
<div class="form-floating">
@ -31,6 +42,19 @@
<p class="mt-5 mb-3 text-muted">WiTTY: Web-based Interactive TTY</p>
</form>
</main>
<script>
document.addEventListener("DOMContentLoaded", function () {
var element = document.getElementById("authMsg");
var toast = new bootstrap.Toast(element);
toast.show()
setTimeout(() => {
toast.hide()
}, 1500)
});
</script>
</body>
</html>

View File

@ -248,8 +248,8 @@ out:
if err != nil {
log.Println("Failed to marshal record", err)
} else {
tc.record.Write(jbuf)
tc.record.Write([]byte(",")) // write a deliminator
tc.record.Write(jbuf)
}
tc.lastRecTime = time.Now()
@ -259,7 +259,7 @@ out:
var err error
if cmd == recordCmd {
// use the session ID and current as file name
fname := "./records/" + tc.Name + "_" + strconv.FormatInt(time.Now().Unix(), 16) + ".rec"
fname := "./records/" + tc.Name + "_" + strconv.FormatInt(time.Now().Unix(), 16) + ".scr"
tc.record, err = os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
@ -268,16 +268,14 @@ out:
}
tc.record.Write([]byte("[")) // write a [ for an array of json objs
// write a dummy record to clear the screen.
tc.lastRecTime = time.Now()
jbuf, _ := json.Marshal(WriteRecord{Dur: time.Since(tc.lastRecTime), Data: []byte("\033[2J\033[H")})
tc.record.Write(jbuf)
} else {
fsinfo, err := tc.record.Stat()
if err == nil {
tc.record.Truncate(fsinfo.Size() - 1)
tc.record.Seek(0, 2) // truncate does not change read/write location
tc.record.Write([]byte("]"))
}
tc.record.Write([]byte("]"))
tc.record.Close()
tc.record = nil
}

View File

@ -9,9 +9,16 @@ import (
)
const (
userkey = "user"
userkey = "authorized_user"
loginKey = "login_msg"
)
func leftLoginMsg(c *gin.Context, msg string) {
session := sessions.Default(c)
session.Set(loginKey, msg)
session.Save()
}
func login(c *gin.Context) {
session := sessions.Default(c)
@ -20,64 +27,66 @@ func login(c *gin.Context) {
// Validate form input
if strings.Trim(username, " ") == "" || strings.Trim(passwd, " ") == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Username/password can't be empty"})
leftLoginMsg(c, "User name or password cannot be empty")
c.Redirect(http.StatusSeeOther, "/login")
return
}
// Check for username and password match, usually from a database
if username != "hello" || passwd != "world" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"})
leftLoginMsg(c, "Username/password does not match")
c.Redirect(http.StatusSeeOther, "/login")
return
}
// Save the username in the session
session.Set(userkey, username) // In real world usage you'd set this to the users ID
session.Set(userkey, username)
if err := session.Save(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save session"})
leftLoginMsg(c, "Failed to save session data")
c.Redirect(http.StatusSeeOther, "/login")
return
}
host = &c.Request.Host
c.Redirect(http.StatusSeeOther, "/")
}
func logout(c *gin.Context) {
session := sessions.Default(c)
user := session.Get(userkey)
if user == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid session token"})
return
}
session.Delete(userkey)
if err := session.Save(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save session"})
return
if user != nil {
session.Delete(userkey)
session.Save()
}
leftLoginMsg(c, "Welcome to WiTTY")
c.Redirect(http.StatusFound, "/login")
}
// AuthRequired is a simple middleware to check the session
func AuthRequired(c *gin.Context) {
if (c.Request.URL.String() == "/login") ||
strings.HasPrefix(c.Request.URL.String(), "/assets") {
c.Next()
return
}
session := sessions.Default(c)
user := session.Get(userkey)
if user == nil {
// Abort the request with the appropriate error code
leftLoginMsg(c, "Not authorized, login first")
c.Redirect(http.StatusTemporaryRedirect, "/login")
c.Abort()
return
}
// Continue down the chain to handler etc
c.Next()
}
func loginPage(c *gin.Context) {
c.HTML(http.StatusOK, "login.html", gin.H{})
session := sessions.Default(c)
msg := session.Get(loginKey)
if msg == nil {
msg = "Login first"
}
c.HTML(http.StatusOK, "login.html", gin.H{"msg": msg})
}

View File

@ -63,7 +63,7 @@ func collectRecords(c *gin.Context, cmd string) (records []RecordedSession) {
if err == nil {
for _, finfo := range files {
fname := finfo.Name()
if !strings.HasSuffix(fname, ".rec") {
if !strings.HasSuffix(fname, ".scr") {
continue
}
fsize := finfo.Size() / 1024

View File

@ -44,44 +44,44 @@ func StartWeb(fp *os.File, cmd []string) {
// so login can survive server reboot
store := sessions.NewCookieStore([]byte(uniuri.NewLen(32)))
rt.Use(sessions.Sessions("witty-session", store))
rt.Use(AuthRequired)
rt.SetTrustedProxies(nil)
rt.LoadHTMLGlob("./assets/template/*")
// Fill in the index page
rt.GET("/", indexPage)
rt.GET("/login", loginPage)
rt.POST("/login", login)
rt.GET("/logout", logout)
// to update the tabs of current interactive and saved sessions
rt.GET("/update/:active", updateIndex)
// create a new interactive session
rt.GET("/new", newInteractive)
rt.GET("/ws_new/:id", newTermConn)
// create a viewer of an interactive session
rt.GET("/view/:id", viewPage)
rt.GET("/ws_view/:id", newViewWS)
// start/stop recording the session
rt.GET("/record/:id", startRecord)
rt.GET("/stop/:id", stopRecord)
// create a viewer of an interactive session
rt.GET("/replay/:id", replayPage)
// delete a recording
rt.GET("/delete/:fname", delRec)
// handle static files
rt.Static("/assets", "./assets")
rt.Static("/records", "./records")
rt.GET("/favicon.ico", favIcon)
rt.GET("/login", loginPage)
rt.POST("/login", login)
g1 := rt.Group("/", AuthRequired)
// Fill in the index page
g1.GET("/", indexPage)
g1.GET("/logout", logout)
// to update the tabs of current interactive and saved sessions
g1.GET("/update/:active", updateIndex)
// create a new interactive session
g1.GET("/new", newInteractive)
g1.GET("/ws_new/:id", newTermConn)
// create a viewer of an interactive session
g1.GET("/view/:id", viewPage)
g1.GET("/ws_view/:id", newViewWS)
// start/stop recording the session
g1.GET("/record/:id", startRecord)
g1.GET("/stop/:id", stopRecord)
// create a viewer of an interactive session
g1.GET("/replay/:id", replayPage)
// delete a recording
g1.GET("/delete/:fname", delRec)
term_conn.Init(checkOrigin)
rt.RunTLS(":8080", "./tls/cert.pem", "./tls/private-key.pem")
}