From 8dc94bd5bc07ec7e974c89da4196e461259316a3 Mon Sep 17 00:00:00 2001 From: Bagas Aulia Rezki Date: Thu, 20 Jun 2024 17:33:56 +0700 Subject: [PATCH 1/4] Move back button --- view/user/totp/setup.templ | 42 ++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/view/user/totp/setup.templ b/view/user/totp/setup.templ index 1aedcad..d2c3679 100644 --- a/view/user/totp/setup.templ +++ b/view/user/totp/setup.templ @@ -9,29 +9,31 @@ templ content(title string, qrcode string, code string, user types.User) { @layout.Base(title){ @layout.Navbar(user)
+ + + + + + Back +
- - - - - -

Set up Two-Factor Authentication

Secure your account with time-based one-time passwords (TOTP).

From b732262dcdc055a74876abd7cd8bffb159181fc7 Mon Sep 17 00:00:00 2001 From: Bagas Aulia Rezki Date: Thu, 20 Jun 2024 17:53:03 +0700 Subject: [PATCH 2/4] Update HTTP route function to new syntax in Go 1.22+ --- routes/routes.go | 210 ++++++++++++++--------------------------------- 1 file changed, 63 insertions(+), 147 deletions(-) diff --git a/routes/routes.go b/routes/routes.go index 8948521..be92670 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -25,194 +25,110 @@ 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("GET /user/totp/setup", func(w http.ResponseWriter, r *http.Request) { + middleware.Auth(userHandlerTotpSetup.GET, 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("POST /user/totp/setup", func(w http.ResponseWriter, r *http.Request) { + middleware.Auth(userHandlerTotpSetup.POST, 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("GET /upload", func(w http.ResponseWriter, r *http.Request) { + middleware.Auth(uploadHandler.GET, 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("POST /upload/{id}", func(w http.ResponseWriter, r *http.Request) { + middleware.Auth(uploadHandler.POST, w, r) }) - handler.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { + 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") }) From 949099124e07251bbc503548746f5d2bdf52a7be Mon Sep 17 00:00:00 2001 From: Bagas Aulia Rezki Date: Thu, 20 Jun 2024 18:15:49 +0700 Subject: [PATCH 3/4] Add functionality for session termination --- handler/user/session/terminate/terminate.go | 23 ++++++++++++++ routes/routes.go | 5 +++ view/user/user.templ | 35 ++++++++++++++++++--- 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 handler/user/session/terminate/terminate.go diff --git a/handler/user/session/terminate/terminate.go b/handler/user/session/terminate/terminate.go new file mode 100644 index 0000000..95caf0a --- /dev/null +++ b/handler/user/session/terminate/terminate.go @@ -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 + } +} diff --git a/routes/routes.go b/routes/routes.go index be92670..6ff4877 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -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" @@ -92,6 +93,10 @@ func SetupRoutes() *http.ServeMux { middleware.Auth(userHandler.GET, w, r) }) + handler.HandleFunc("DELETE /user/session/terminate/{id}", func(w http.ResponseWriter, r *http.Request) { + middleware.Auth(userSessionTerminateHandler.DELETE, w, r) + }) + handler.HandleFunc("GET /user/totp/setup", func(w http.ResponseWriter, r *http.Request) { middleware.Auth(userHandlerTotpSetup.GET, w, r) }) diff --git a/view/user/user.templ b/view/user/user.templ index f6f2084..8888dcf 100644 --- a/view/user/user.templ +++ b/view/user/user.templ @@ -109,7 +109,7 @@ templ content(title string, user types.User, ListSession []*session.SessionInfo) - + for _, ses := range ListSession { @@ -122,13 +122,13 @@ templ content(title string, user types.User, ListSession []*session.SessionInfo) {ses.AccessAt} - + } @@ -262,6 +262,33 @@ templ content(title string, user types.User, ListSession []*session.SessionInfo) } } +templ SessionTable(ListSession []*session.SessionInfo){ + + for _, ses := range ListSession { + + {ses.IP} + + {ses.Browser + ses.Version} + + {ses.OS + ses.OSVersion} + + {ses.AccessAt} + + + + + + } + +} + templ Main(title string, user types.User, ListSession []*session.SessionInfo) { @content(title, user, ListSession) } \ No newline at end of file From fd2a7d110a8f46be5f6616cf9b4e5f5c8862eec2 Mon Sep 17 00:00:00 2001 From: Bagas Aulia Rezki Date: Thu, 20 Jun 2024 18:46:53 +0700 Subject: [PATCH 4/4] Improve 2FA verification page messaging --- handler/auth/totp/totp.go | 17 ++++++++++++++--- handler/user/totp/setup.go | 34 +++++++++++++++++++++++----------- view/totp/totp.templ | 23 ++++++++++++++++++----- view/user/totp/setup.templ | 30 ++++++++++++++++++++++++++---- 4 files changed, 81 insertions(+), 23 deletions(-) diff --git a/handler/auth/totp/totp.go b/handler/auth/totp/totp.go index e095fb5..cce1782 100644 --- a/handler/auth/totp/totp.go +++ b/handler/auth/totp/totp.go @@ -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 + } + } } diff --git a/handler/user/totp/setup.go b/handler/user/totp/setup.go index 065d8c6..6c60740 100644 --- a/handler/user/totp/setup.go +++ b/handler/user/totp/setup.go @@ -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 diff --git a/view/totp/totp.templ b/view/totp/totp.templ index 3694d1c..a645c11 100644 --- a/view/totp/totp.templ +++ b/view/totp/totp.templ @@ -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){
+ switch msg.Code { + case 0: + + }

Verify Your Identity

- 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.

@@ -36,7 +49,7 @@ templ content(title string) {
+ switch msg.Code { + case 0: + + case 1: + + }

Backup Code:

-
12345-67890
+
----|----

TOTP Secret:

{code} @@ -94,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) } \ No newline at end of file