Merge pull request #29 from fossyy/staging

Staging
This commit is contained in:
2024-06-20 18:47:49 +07:00
committed by GitHub
7 changed files with 225 additions and 194 deletions

View File

@ -2,7 +2,6 @@ package totpHandler
import (
"errors"
"fmt"
"github.com/fossyy/filekeeper/session"
"github.com/fossyy/filekeeper/types"
"github.com/fossyy/filekeeper/utils"
@ -19,7 +18,10 @@ func GET(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
return
}
component := totpView.Main("Filekeeper - 2FA Page")
component := totpView.Main("Filekeeper - 2FA Page", types.Message{
Code: 1,
Message: "",
})
err := component.Render(r.Context(), w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
@ -73,7 +75,16 @@ func POST(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, cookie.Value, http.StatusSeeOther)
return
} else {
fmt.Fprint(w, "wrong")
component := totpView.Main("Filekeeper - 2FA Page", types.Message{
Code: 0,
Message: "Incorrect code. Please try again with the latest code from your authentication app.",
})
err := component.Render(r.Context(), w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}
}

View File

@ -0,0 +1,23 @@
package userSessionTerminateHandler
import (
"github.com/fossyy/filekeeper/session"
userView "github.com/fossyy/filekeeper/view/user"
"net/http"
)
func DELETE(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
_, mySession, _ := session.GetSession(r)
otherSession, _ := session.Get(id)
otherSession.Delete()
session.RemoveSessionInfo(mySession.Email, otherSession.ID)
component := userView.SessionTable(session.GetSessions(mySession.Email))
err := component.Render(r.Context(), w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}

View File

@ -42,7 +42,10 @@ func GET(w http.ResponseWriter, r *http.Request) {
return
}
component := userTotpSetupView.Main("Filekeeper - 2FA Setup Page", base64Str, secret, userSession)
component := userTotpSetupView.Main("Filekeeper - 2FA Setup Page", base64Str, secret, userSession, types.Message{
Code: 3,
Message: "",
})
if err := component.Render(r.Context(), w); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
@ -59,25 +62,34 @@ func POST(w http.ResponseWriter, r *http.Request) {
secret := r.Form.Get("secret")
totp := gotp.NewDefaultTOTP(secret)
userSession := r.Context().Value("user").(types.User)
uri := totp.ProvisioningUri(userSession.Email, "filekeeper")
base64Str, err := generateQRCode(uri)
if err != nil {
fmt.Printf("%v\n", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if totp.Verify(code, time.Now().Unix()) {
if err := db.DB.InitializeTotp(userSession.Email, secret); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
cache.DeleteUser(userSession.Email)
fmt.Fprint(w, "Authentication successful! Access granted.")
return
} else {
uri := totp.ProvisioningUri(userSession.Email, "filekeeper")
base64Str, err := generateQRCode(uri)
if err != nil {
fmt.Printf("%v\n", err)
component := userTotpSetupView.Main("Filekeeper - 2FA Setup Page", base64Str, secret, userSession, types.Message{
Code: 1,
Message: "Your TOTP setup is complete! Your account is now more secure.",
})
if err := component.Render(r.Context(), w); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
component := userTotpSetupView.Main("Filekeeper - 2FA Setup Page", base64Str, secret, userSession)
return
} else {
component := userTotpSetupView.Main("Filekeeper - 2FA Setup Page", base64Str, secret, userSession, types.Message{
Code: 0,
Message: "The code you entered is incorrect. Please double-check the code and try again.",
})
if err := component.Render(r.Context(), w); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return

View File

@ -17,6 +17,7 @@ import (
uploadHandler "github.com/fossyy/filekeeper/handler/upload"
"github.com/fossyy/filekeeper/handler/upload/initialisation"
userHandler "github.com/fossyy/filekeeper/handler/user"
userSessionTerminateHandler "github.com/fossyy/filekeeper/handler/user/session/terminate"
userHandlerTotpSetup "github.com/fossyy/filekeeper/handler/user/totp"
"github.com/fossyy/filekeeper/middleware"
"net/http"
@ -25,194 +26,114 @@ import (
func SetupRoutes() *http.ServeMux {
handler := http.NewServeMux()
handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/":
switch r.Method {
case http.MethodGet:
indexHandler.GET(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
default:
w.WriteHeader(http.StatusNotFound)
}
handler.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
indexHandler.GET(w, r)
})
authRouter := http.NewServeMux()
handler.Handle("/auth/", http.StripPrefix("/auth", authRouter))
authRouter.HandleFunc("/google", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Guest(googleOauthHandler.GET, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
handler.HandleFunc("GET /auth/google", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(googleOauthHandler.GET, w, r)
})
authRouter.HandleFunc("/totp", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Guest(totpHandler.GET, w, r)
case http.MethodPost:
middleware.Guest(totpHandler.POST, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
handler.HandleFunc("GET /auth/totp", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(totpHandler.GET, w, r)
})
authRouter.HandleFunc("/google/callback", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Guest(googleOauthCallbackHandler.GET, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
handler.HandleFunc("POST /auth/totp", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(totpHandler.POST, w, r)
})
authRouter.HandleFunc("/google/setup/{code}", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Guest(googleOauthSetupHandler.GET, w, r)
case http.MethodPost:
middleware.Guest(googleOauthSetupHandler.POST, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
handler.HandleFunc("GET /auth/google/callback", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(googleOauthCallbackHandler.GET, w, r)
})
handler.HandleFunc("/signin", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Guest(signinHandler.GET, w, r)
case http.MethodPost:
middleware.Guest(signinHandler.POST, w, r)
}
handler.HandleFunc("GET /auth/google/setup/{code}", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(googleOauthSetupHandler.GET, w, r)
})
handler.HandleFunc("POST /auth/google/setup/{code}", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(googleOauthSetupHandler.POST, w, r)
})
signupRouter := http.NewServeMux()
handler.Handle("/signup/", http.StripPrefix("/signup", signupRouter))
signupRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Guest(signupHandler.GET, w, r)
case http.MethodPost:
middleware.Guest(signupHandler.POST, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
handler.HandleFunc("GET /signin", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(signinHandler.GET, w, r)
})
signupRouter.HandleFunc("/verify/{code}", func(w http.ResponseWriter, r *http.Request) {
handler.HandleFunc("POST /signin", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(signinHandler.POST, w, r)
})
handler.HandleFunc("GET /signup", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(signupHandler.GET, w, r)
})
handler.HandleFunc("POST /signup", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(signupHandler.POST, w, r)
})
handler.HandleFunc("GET /signup/verify/{code}", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(signupVerifyHandler.GET, w, r)
})
forgotPasswordRouter := http.NewServeMux()
handler.Handle("/forgot-password/", http.StripPrefix("/forgot-password", forgotPasswordRouter))
forgotPasswordRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Guest(forgotPasswordHandler.GET, w, r)
case http.MethodPost:
middleware.Guest(forgotPasswordHandler.POST, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
handler.HandleFunc("GET /forgot-password", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(forgotPasswordHandler.GET, w, r)
})
forgotPasswordRouter.HandleFunc("/verify/{code}", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Guest(forgotPasswordVerifyHandler.GET, w, r)
case http.MethodPost:
middleware.Guest(forgotPasswordVerifyHandler.POST, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
handler.HandleFunc("POST /forgot-password", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(forgotPasswordHandler.POST, w, r)
})
handler.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Auth(userHandler.GET, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
handler.HandleFunc("GET /forgot-password/verify/{code}", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(forgotPasswordVerifyHandler.GET, w, r)
})
handler.HandleFunc("/user/totp/setup", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Auth(userHandlerTotpSetup.GET, w, r)
case http.MethodPost:
middleware.Auth(userHandlerTotpSetup.POST, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
handler.HandleFunc("POST /forgot-password/verify/{code}", func(w http.ResponseWriter, r *http.Request) {
middleware.Guest(forgotPasswordVerifyHandler.POST, w, r)
})
// Upload router
uploadRouter := http.NewServeMux()
handler.Handle("/upload/", http.StripPrefix("/upload", uploadRouter))
uploadRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Auth(uploadHandler.GET, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
handler.HandleFunc("GET /user", func(w http.ResponseWriter, r *http.Request) {
middleware.Auth(userHandler.GET, w, r)
})
uploadRouter.HandleFunc("/{id}", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
middleware.Auth(uploadHandler.POST, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
handler.HandleFunc("DELETE /user/session/terminate/{id}", func(w http.ResponseWriter, r *http.Request) {
middleware.Auth(userSessionTerminateHandler.DELETE, w, r)
})
uploadRouter.HandleFunc("/init", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
middleware.Auth(initialisation.POST, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
handler.HandleFunc("GET /user/totp/setup", func(w http.ResponseWriter, r *http.Request) {
middleware.Auth(userHandlerTotpSetup.GET, w, r)
})
// Download router
downloadRouter := http.NewServeMux()
handler.Handle("/download/", http.StripPrefix("/download", downloadRouter))
downloadRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Auth(downloadHandler.GET, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
handler.HandleFunc("POST /user/totp/setup", func(w http.ResponseWriter, r *http.Request) {
middleware.Auth(userHandlerTotpSetup.POST, w, r)
})
downloadRouter.HandleFunc("/{id}", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
downloadFileHandler.GET(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
handler.HandleFunc("GET /upload", func(w http.ResponseWriter, r *http.Request) {
middleware.Auth(uploadHandler.GET, w, r)
})
handler.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
handler.HandleFunc("POST /upload/{id}", func(w http.ResponseWriter, r *http.Request) {
middleware.Auth(uploadHandler.POST, w, r)
})
handler.HandleFunc("POST /upload/init", func(w http.ResponseWriter, r *http.Request) {
middleware.Auth(initialisation.POST, w, r)
})
handler.HandleFunc("GET /download", func(w http.ResponseWriter, r *http.Request) {
middleware.Auth(downloadHandler.GET, w, r)
})
handler.HandleFunc("GET /download/{id}", func(w http.ResponseWriter, r *http.Request) {
downloadFileHandler.GET(w, r)
})
handler.HandleFunc("GET /logout", func(w http.ResponseWriter, r *http.Request) {
middleware.Auth(logoutHandler.GET, w, r)
})
handler.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
handler.HandleFunc("GET /robots.txt", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "public/robots.txt")
})
handler.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
handler.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "public/favicon.ico")
})

View File

@ -2,17 +2,30 @@ package totpView
import (
"github.com/fossyy/filekeeper/view/layout"
"github.com/fossyy/filekeeper/types"
)
templ content(title string) {
templ content(title string, msg types.Message) {
@layout.Base(title){
<main class="container mx-auto px-4 py-12 md:px-6 md:py-16 lg:py-10">
<div class="flex min-h-screen items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-8">
<div>
switch msg.Code {
case 0:
<div class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50" role="alert">
<svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
</svg>
<span class="sr-only">Info</span>
<div>
<span class="font-medium">Error!</span> {msg.Message}
</div>
</div>
}
<h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-foreground">Verify Your Identity</h2>
<p class="mt-2 text-center text-sm text-muted-foreground">
Enter the 6-digit code sent to your registered device to complete the login process.
Please enter the 6-digit code generated by your authentication app to complete the login process.
</p>
</div>
<form class="space-y-6" method="POST">
@ -36,7 +49,7 @@ templ content(title string) {
</div>
<div>
<button
class="items-center whitespace-nowrap ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 flex w-full justify-center rounded-md bg-primary py-2 px-4 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
class="items-center whitespace-nowrap ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 flex w-full justify-center rounded-md bg-black py-2 px-4 text-sm font-medium text-primary-foreground shadow-sm text-white hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
type="submit"
>
Verify Code
@ -49,6 +62,6 @@ templ content(title string) {
}
}
templ Main(title string) {
@content(title)
templ Main(title string, msg types.Message) {
@content(title, msg)
}

View File

@ -5,33 +5,35 @@ import (
"github.com/fossyy/filekeeper/types"
)
templ content(title string, qrcode string, code string, user types.User) {
templ content(title string, qrcode string, code string, user types.User, msg types.Message) {
@layout.Base(title){
@layout.Navbar(user)
<main class="container mx-auto px-4 py-12 md:px-6 md:py-16 lg:py-10">
<a
class="inline-flex items-center space-x-2 rounded-md bg-muted px-4 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted/80 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
href="/user" hx-get="/user" hx-swap="outerHTML" hx-push-url="true" hx-target="#content"
rel="ugc"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
>
<path d="m12 19-7-7 7-7"></path>
<path d="M19 12H5"></path>
</svg>
<span>Back</span>
</a>
<div class="mx-auto max-w-md px-4 py-12 sm:px-6 lg:px-8">
<div class="space-y-6 text-center">
<div class="flex items-center">
<a
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-slate-200 hover:text-accent-foreground h-10 w-10 mr-4"
href="/user" hx-get="/user" hx-swap="outerHTML" hx-push-url="true" hx-target="#content"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-5 w-5"
>
<path d="m12 19-7-7 7-7"></path>
<path d="M19 12H5"></path>
</svg>
</a>
<h1 class="text-3xl font-bold">Set up Two-Factor Authentication</h1>
</div>
<p class="text-muted-foreground">Secure your account with time-based one-time passwords (TOTP).</p>
@ -48,6 +50,28 @@ templ content(title string, qrcode string, code string, user types.User) {
</div>
<div class="rounded-lg border rounded-lg bg-muted p-6bg-card text-card-foreground shadow-sm mt-5" data-v0-t="card">
<div class="p-6 space-y-6">
switch msg.Code {
case 0:
<div class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50" role="alert">
<svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
</svg>
<span class="sr-only">Info</span>
<div>
<span class="font-medium">Error!</span> {msg.Message}
</div>
</div>
case 1:
<div class="flex items-center p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50" role="alert">
<svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
</svg>
<span class="sr-only">Info</span>
<div>
<span class="font-medium">Success!</span> {msg.Message}
</div>
</div>
}
<div class="flex items-center justify-center">
<img
src={"data:image/png;base64," + qrcode}
@ -60,7 +84,7 @@ templ content(title string, qrcode string, code string, user types.User) {
</div>
<div class="mt-6 space-y-2">
<p class="font-medium">Backup Code:</p>
<div class="rounded-md bg-background px-4 py-2 text-sm font-mono text-muted-foreground">12345-67890</div>
<div class="rounded-md bg-background px-4 py-2 text-sm font-mono text-muted-foreground">----|----</div>
<p class="font-medium">TOTP Secret:</p>
<div class="rounded-md bg-background px-4 py-2 text-sm font-mono text-muted-foreground">
{code}
@ -92,6 +116,6 @@ templ content(title string, qrcode string, code string, user types.User) {
}
}
templ Main(title string, qrcode string, code string, user types.User) {
@content(title, qrcode, code, user)
templ Main(title string, qrcode string, code string, user types.User, msg types.Message) {
@content(title, qrcode, code, user, msg)
}

View File

@ -109,7 +109,7 @@ templ content(title string, user types.User, ListSession []*session.SessionInfo)
</th>
</tr>
</thead>
<tbody class="[&amp;_tr:last-child]:border-0">
<tbody class="[&amp;_tr:last-child]:border-0" id="session-tables">
for _, ses := range ListSession {
<tr
class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
@ -122,13 +122,13 @@ templ content(title string, user types.User, ListSession []*session.SessionInfo)
<td class="p-4 align-middle [&amp;:has([role=checkbox])]:pr-0">{ses.AccessAt}
</td>
<td class="p-4 align-middle [&amp;:has([role=checkbox])]:pr-0">
<a
<button
class="hover:bg-gray-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
type="button" id="radix-:rq:" aria-haspopup="menu"
aria-expanded="false" data-state="closed"
href="/">
hx-delete={"/user/session/terminate/"+ses.SessionID} hx-target="#session-tables" hx-swap="outerHTML">
Terminate
</a>
</button>
</td>
</tr>
}
@ -262,6 +262,33 @@ templ content(title string, user types.User, ListSession []*session.SessionInfo)
}
}
templ SessionTable(ListSession []*session.SessionInfo){
<tbody class="[&amp;_tr:last-child]:border-0" id="session-tables">
for _, ses := range ListSession {
<tr
class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
<td class="p-4 align-middle [&amp;:has([role=checkbox])]:pr-0">{ses.IP}
</td>
<td class="p-4 align-middle [&amp;:has([role=checkbox])]:pr-0">{ses.Browser + ses.Version}
</td>
<td class="p-4 align-middle [&amp;:has([role=checkbox])]:pr-0">{ses.OS + ses.OSVersion}
</td>
<td class="p-4 align-middle [&amp;:has([role=checkbox])]:pr-0">{ses.AccessAt}
</td>
<td class="p-4 align-middle [&amp;:has([role=checkbox])]:pr-0">
<button
class="hover:bg-gray-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
type="button" id="radix-:rq:" aria-haspopup="menu"
aria-expanded="false" data-state="closed"
hx-delete={"/user/session/terminate/"+ses.SessionID} hx-target="#session-tables" hx-swap="outerHTML">
Terminate
</button>
</td>
</tr>
}
</tbody>
}
templ Main(title string, user types.User, ListSession []*session.SessionInfo) {
@content(title, user, ListSession)
}