Added error 503 popup

This commit is contained in:
2025-02-18 20:51:22 +11:00
parent 0ece08726d
commit 1f7a9e08e6
9 changed files with 291 additions and 177 deletions

View File

@@ -3,6 +3,7 @@ package handlers
import ( import (
"context" "context"
"net/http" "net/http"
"time"
"projectreshoot/contexts" "projectreshoot/contexts"
"projectreshoot/cookies" "projectreshoot/cookies"
@@ -47,35 +48,41 @@ func HandleChangeUsername(
) http.Handler { ) http.Handler {
return http.HandlerFunc( return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
WithTransaction(w, r, logger, conn, ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
func(ctx context.Context, tx *db.SafeTX, w http.ResponseWriter, r *http.Request) { defer cancel()
r.ParseForm()
newUsername := r.FormValue("username") // Start the transaction
unique, err := db.CheckUsernameUnique(ctx, tx, newUsername) tx, err := conn.Begin(ctx)
if err != nil { if err != nil {
tx.Rollback() logger.Warn().Err(err).Msg("Error updating username")
logger.Error().Err(err).Msg("Error updating username") w.WriteHeader(http.StatusServiceUnavailable)
w.WriteHeader(http.StatusInternalServerError) return
return }
} r.ParseForm()
if !unique { newUsername := r.FormValue("username")
tx.Rollback() unique, err := db.CheckUsernameUnique(ctx, tx, newUsername)
account.ChangeUsername("Username is taken", newUsername). if err != nil {
Render(r.Context(), w) tx.Rollback()
return logger.Error().Err(err).Msg("Error updating username")
} w.WriteHeader(http.StatusInternalServerError)
user := contexts.GetUser(r.Context()) return
err = user.ChangeUsername(ctx, tx, newUsername) }
if err != nil { if !unique {
tx.Rollback() tx.Rollback()
logger.Error().Err(err).Msg("Error updating username") account.ChangeUsername("Username is taken", newUsername).
w.WriteHeader(http.StatusInternalServerError) Render(r.Context(), w)
return return
} }
tx.Commit() user := contexts.GetUser(r.Context())
w.Header().Set("HX-Refresh", "true") err = user.ChangeUsername(ctx, tx, newUsername)
}, if err != nil {
) tx.Rollback()
logger.Error().Err(err).Msg("Error updating username")
w.WriteHeader(http.StatusInternalServerError)
return
}
tx.Commit()
w.Header().Set("HX-Refresh", "true")
}, },
) )
} }
@@ -87,29 +94,35 @@ func HandleChangeBio(
) http.Handler { ) http.Handler {
return http.HandlerFunc( return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
WithTransaction(w, r, logger, conn, ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
func(ctx context.Context, tx *db.SafeTX, w http.ResponseWriter, r *http.Request) { defer cancel()
r.ParseForm()
newBio := r.FormValue("bio") // Start the transaction
leng := len([]rune(newBio)) tx, err := conn.Begin(ctx)
if leng > 128 { if err != nil {
tx.Rollback() logger.Warn().Err(err).Msg("Error updating bio")
account.ChangeBio("Bio limited to 128 characters", newBio). w.WriteHeader(http.StatusServiceUnavailable)
Render(r.Context(), w) return
return }
} r.ParseForm()
user := contexts.GetUser(r.Context()) newBio := r.FormValue("bio")
err := user.ChangeBio(ctx, tx, newBio) leng := len([]rune(newBio))
if err != nil { if leng > 128 {
tx.Rollback() tx.Rollback()
logger.Error().Err(err).Msg("Error updating bio") account.ChangeBio("Bio limited to 128 characters", newBio).
w.WriteHeader(http.StatusInternalServerError) Render(r.Context(), w)
return return
} }
tx.Commit() user := contexts.GetUser(r.Context())
w.Header().Set("HX-Refresh", "true") err = user.ChangeBio(ctx, tx, newBio)
}, if err != nil {
) tx.Rollback()
logger.Error().Err(err).Msg("Error updating bio")
w.WriteHeader(http.StatusInternalServerError)
return
}
tx.Commit()
w.Header().Set("HX-Refresh", "true")
}, },
) )
} }
@@ -137,26 +150,32 @@ func HandleChangePassword(
) http.Handler { ) http.Handler {
return http.HandlerFunc( return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
WithTransaction(w, r, logger, conn, ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
func(ctx context.Context, tx *db.SafeTX, w http.ResponseWriter, r *http.Request) { defer cancel()
newPass, err := validateChangePassword(ctx, tx, r)
if err != nil { // Start the transaction
tx.Rollback() tx, err := conn.Begin(ctx)
account.ChangePassword(err.Error()).Render(r.Context(), w) if err != nil {
return logger.Warn().Err(err).Msg("Error updating password")
} w.WriteHeader(http.StatusServiceUnavailable)
user := contexts.GetUser(r.Context()) return
err = user.SetPassword(ctx, tx, newPass) }
if err != nil { newPass, err := validateChangePassword(ctx, tx, r)
tx.Rollback() if err != nil {
logger.Error().Err(err).Msg("Error updating password") tx.Rollback()
w.WriteHeader(http.StatusInternalServerError) account.ChangePassword(err.Error()).Render(r.Context(), w)
return return
} }
tx.Commit() user := contexts.GetUser(r.Context())
w.Header().Set("HX-Refresh", "true") err = user.SetPassword(ctx, tx, newPass)
}, if err != nil {
) tx.Rollback()
logger.Error().Err(err).Msg("Error updating password")
w.WriteHeader(http.StatusInternalServerError)
return
}
tx.Commit()
w.Header().Set("HX-Refresh", "true")
}, },
) )
} }

View File

@@ -3,6 +3,7 @@ package handlers
import ( import (
"context" "context"
"net/http" "net/http"
"time"
"projectreshoot/config" "projectreshoot/config"
"projectreshoot/cookies" "projectreshoot/cookies"
@@ -55,34 +56,41 @@ func HandleLoginRequest(
) http.Handler { ) http.Handler {
return http.HandlerFunc( return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
WithTransaction(w, r, logger, conn, ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
func(ctx context.Context, tx *db.SafeTX, w http.ResponseWriter, r *http.Request) { defer cancel()
r.ParseForm()
user, err := validateLogin(ctx, tx, r)
if err != nil {
tx.Rollback()
if err.Error() != "Username or password incorrect" {
logger.Warn().Caller().Err(err).Msg("Login request failed")
w.WriteHeader(http.StatusInternalServerError)
} else {
form.LoginForm(err.Error()).Render(r.Context(), w)
}
return
}
rememberMe := checkRememberMe(r) // Start the transaction
err = cookies.SetTokenCookies(w, r, config, user, true, rememberMe) tx, err := conn.Begin(ctx)
if err != nil { if err != nil {
tx.Rollback() logger.Warn().Err(err).Msg("Failed to set token cookies")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusServiceUnavailable)
logger.Warn().Caller().Err(err).Msg("Failed to set token cookies") return
return }
} r.ParseForm()
user, err := validateLogin(ctx, tx, r)
if err != nil {
tx.Rollback()
if err.Error() != "Username or password incorrect" {
logger.Warn().Caller().Err(err).Msg("Login request failed")
w.WriteHeader(http.StatusInternalServerError)
} else {
form.LoginForm(err.Error()).Render(r.Context(), w)
}
return
}
tx.Commit() rememberMe := checkRememberMe(r)
pageFrom := cookies.CheckPageFrom(w, r) err = cookies.SetTokenCookies(w, r, config, user, true, rememberMe)
w.Header().Set("HX-Redirect", pageFrom) if err != nil {
}) tx.Rollback()
w.WriteHeader(http.StatusInternalServerError)
logger.Warn().Caller().Err(err).Msg("Failed to set token cookies")
return
}
tx.Commit()
pageFrom := cookies.CheckPageFrom(w, r)
w.Header().Set("HX-Redirect", pageFrom)
}, },
) )
} }

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"net/http" "net/http"
"strings" "strings"
"time"
"projectreshoot/config" "projectreshoot/config"
"projectreshoot/cookies" "projectreshoot/cookies"
@@ -86,20 +87,27 @@ func HandleLogout(
) http.Handler { ) http.Handler {
return http.HandlerFunc( return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
WithTransaction(w, r, logger, conn, ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
func(ctx context.Context, tx *db.SafeTX, w http.ResponseWriter, r *http.Request) { defer cancel()
err := revokeTokens(config, ctx, tx, r)
if err != nil { // Start the transaction
tx.Rollback() tx, err := conn.Begin(ctx)
logger.Error().Err(err).Msg("Error occured on user logout") if err != nil {
w.WriteHeader(http.StatusInternalServerError) logger.Warn().Err(err).Msg("Error occured on user logout")
return w.WriteHeader(http.StatusServiceUnavailable)
} return
tx.Commit() }
cookies.DeleteCookie(w, "access", "/") err = revokeTokens(config, ctx, tx, r)
cookies.DeleteCookie(w, "refresh", "/") if err != nil {
w.Header().Set("HX-Redirect", "/login") tx.Rollback()
}) logger.Error().Err(err).Msg("Error occured on user logout")
w.WriteHeader(http.StatusInternalServerError)
return
}
tx.Commit()
cookies.DeleteCookie(w, "access", "/")
cookies.DeleteCookie(w, "refresh", "/")
w.Header().Set("HX-Redirect", "/login")
}, },
) )
} }

View File

@@ -3,6 +3,7 @@ package handlers
import ( import (
"context" "context"
"net/http" "net/http"
"time"
"projectreshoot/config" "projectreshoot/config"
"projectreshoot/contexts" "projectreshoot/contexts"
@@ -105,25 +106,32 @@ func HandleReauthenticate(
) http.Handler { ) http.Handler {
return http.HandlerFunc( return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
WithTransaction(w, r, logger, conn, ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
func(ctx context.Context, tx *db.SafeTX, w http.ResponseWriter, r *http.Request) { defer cancel()
err := validatePassword(r)
if err != nil { // Start the transaction
tx.Rollback() tx, err := conn.Begin(ctx)
w.WriteHeader(445) if err != nil {
form.ConfirmPassword("Incorrect password").Render(r.Context(), w) logger.Warn().Err(err).Msg("Failed to refresh user tokens")
return w.WriteHeader(http.StatusServiceUnavailable)
} return
err = refreshTokens(config, ctx, tx, w, r) }
if err != nil { err = validatePassword(r)
tx.Rollback() if err != nil {
logger.Error().Err(err).Msg("Failed to refresh user tokens") tx.Rollback()
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(445)
return form.ConfirmPassword("Incorrect password").Render(r.Context(), w)
} return
tx.Commit() }
w.WriteHeader(http.StatusOK) err = refreshTokens(config, ctx, tx, w, r)
}) if err != nil {
tx.Rollback()
logger.Error().Err(err).Msg("Failed to refresh user tokens")
w.WriteHeader(http.StatusInternalServerError)
return
}
tx.Commit()
w.WriteHeader(http.StatusOK)
}, },
) )
} }

View File

@@ -3,6 +3,7 @@ package handlers
import ( import (
"context" "context"
"net/http" "net/http"
"time"
"projectreshoot/config" "projectreshoot/config"
"projectreshoot/cookies" "projectreshoot/cookies"
@@ -50,36 +51,42 @@ func HandleRegisterRequest(
) http.Handler { ) http.Handler {
return http.HandlerFunc( return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
WithTransaction(w, r, logger, conn, ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
func(ctx context.Context, tx *db.SafeTX, w http.ResponseWriter, r *http.Request) { defer cancel()
r.ParseForm()
user, err := validateRegistration(ctx, tx, r)
if err != nil {
tx.Rollback()
if err.Error() != "Username is taken" &&
err.Error() != "Passwords do not match" &&
err.Error() != "Password exceeds maximum length of 72 bytes" {
logger.Warn().Caller().Err(err).Msg("Registration request failed")
w.WriteHeader(http.StatusInternalServerError)
} else {
form.RegisterForm(err.Error()).Render(r.Context(), w)
}
return
}
rememberMe := checkRememberMe(r) // Start the transaction
err = cookies.SetTokenCookies(w, r, config, user, true, rememberMe) tx, err := conn.Begin(ctx)
if err != nil { if err != nil {
tx.Rollback() logger.Warn().Err(err).Msg("Failed to set token cookies")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusServiceUnavailable)
logger.Warn().Caller().Err(err).Msg("Failed to set token cookies") return
return }
} r.ParseForm()
tx.Commit() user, err := validateRegistration(ctx, tx, r)
pageFrom := cookies.CheckPageFrom(w, r) if err != nil {
w.Header().Set("HX-Redirect", pageFrom) tx.Rollback()
}, if err.Error() != "Username is taken" &&
) err.Error() != "Passwords do not match" &&
err.Error() != "Password exceeds maximum length of 72 bytes" {
logger.Warn().Caller().Err(err).Msg("Registration request failed")
w.WriteHeader(http.StatusInternalServerError)
} else {
form.RegisterForm(err.Error()).Render(r.Context(), w)
}
return
}
rememberMe := checkRememberMe(r)
err = cookies.SetTokenCookies(w, r, config, user, true, rememberMe)
if err != nil {
tx.Rollback()
w.WriteHeader(http.StatusInternalServerError)
logger.Warn().Caller().Err(err).Msg("Failed to set token cookies")
return
}
tx.Commit()
pageFrom := cookies.CheckPageFrom(w, r)
w.Header().Set("HX-Redirect", pageFrom)
}, },
) )
} }

View File

@@ -6,12 +6,11 @@ import (
"time" "time"
"projectreshoot/db" "projectreshoot/db"
"projectreshoot/view/page"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
func WithTransaction( func removeme(
w http.ResponseWriter, w http.ResponseWriter,
r *http.Request, r *http.Request,
logger *zerolog.Logger, logger *zerolog.Logger,
@@ -22,6 +21,7 @@ func WithTransaction(
w http.ResponseWriter, w http.ResponseWriter,
r *http.Request, r *http.Request,
), ),
onfail func(err error),
) { ) {
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel() defer cancel()
@@ -29,13 +29,7 @@ func WithTransaction(
// Start the transaction // Start the transaction
tx, err := conn.Begin(ctx) tx, err := conn.Begin(ctx)
if err != nil { if err != nil {
logger.Warn().Err(err).Msg("Request failed to start a transaction") onfail(err)
w.WriteHeader(http.StatusServiceUnavailable)
page.Error(
"503",
http.StatusText(503),
"This service is currently unavailable. It could be down for maintenance").
Render(r.Context(), w)
return return
} }

View File

@@ -1,9 +1,9 @@
package popup package popup
templ ErrorPopup() { templ Error500Popup() {
<div <div
x-cloak x-cloak
x-show="showError" x-show="showError500"
class="absolute w-82 left-0 right-0 mt-20 mr-5 ml-auto" class="absolute w-82 left-0 right-0 mt-20 mr-5 ml-auto"
x-transition:enter="transform translate-x-[100%] opacity-0 duration-200" x-transition:enter="transform translate-x-[100%] opacity-0 duration-200"
x-transition:enter-start="opacity-0 translate-x-[100%]" x-transition:enter-start="opacity-0 translate-x-[100%]"
@@ -44,7 +44,7 @@ templ ErrorPopup() {
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
class="size-6 text-subtext0 hover:cursor-pointer" class="size-6 text-subtext0 hover:cursor-pointer"
@click="showError=false" @click="showError500=false"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"

View File

@@ -0,0 +1,63 @@
package popup
templ Error503Popup() {
<div
x-cloak
x-show="showError503"
class="absolute w-82 left-0 right-0 mt-20 mr-5 ml-auto"
x-transition:enter="transform translate-x-[100%] opacity-0 duration-200"
x-transition:enter-start="opacity-0 translate-x-[100%]"
x-transition:enter-end="opacity-100 translate-x-0"
x-transition:leave="opacity-0 duration-200"
x-transition:leave-start="opacity-100 translate-x-0"
x-transition:leave-end="opacity-0 translate-x-[100%]"
>
<div
role="alert"
class="rounded-sm bg-dark-red p-4"
>
<div class="flex justify-between">
<div class="flex items-center gap-2 text-red w-fit">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-5"
>
<path
fill-rule="evenodd"
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355
12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309
0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75
0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0
01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z"
clip-rule="evenodd"
></path>
</svg>
<strong class="block font-medium">Service Unavailable</strong>
</div>
<div class="flex">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6 text-subtext0 hover:cursor-pointer"
@click="showError503=false"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</div>
</div>
<p class="mt-2 text-sm text-red">
The service is currently available. It could be down for maintenance.
Please try again later.
</p>
</div>
</div>
}

View File

@@ -41,11 +41,12 @@ templ Global() {
<script src="https://unpkg.com/alpinejs" defer></script> <script src="https://unpkg.com/alpinejs" defer></script>
<script> <script>
// uncomment this line to enable logging of htmx events // uncomment this line to enable logging of htmx events
// htmx.logAll(); htmx.logAll();
</script> </script>
<script> <script>
const bodyData = { const bodyData = {
showError: false, showError500: false,
showError503: false,
showConfirmPasswordModal: false, showConfirmPasswordModal: false,
handleHtmxBeforeOnLoad(event) { handleHtmxBeforeOnLoad(event) {
const requestPath = event.detail.pathInfo.requestPath; const requestPath = event.detail.pathInfo.requestPath;
@@ -65,8 +66,13 @@ templ Global() {
// internal server error // internal server error
if (errorCode.includes('Code 500')) { if (errorCode.includes('Code 500')) {
this.showError = true; this.showError500 = true;
setTimeout(() => this.showError = false, 6000); setTimeout(() => this.showError500 = false, 6000);
}
// service not available error
if (errorCode.includes('Code 503')) {
this.showError503 = true;
setTimeout(() => this.showError503 = false, 6000);
} }
// user is authorized but needs to refresh their login // user is authorized but needs to refresh their login
@@ -83,7 +89,8 @@ templ Global() {
x-on:htmx:error="handleHtmxError($event)" x-on:htmx:error="handleHtmxError($event)"
x-on:htmx:before-on-load="handleHtmxBeforeOnLoad($event)" x-on:htmx:before-on-load="handleHtmxBeforeOnLoad($event)"
> >
@popup.ErrorPopup() @popup.Error500Popup()
@popup.Error503Popup()
@popup.ConfirmPasswordModal() @popup.ConfirmPasswordModal()
<div <div
id="main-content" id="main-content"