Compare commits

...

2 commits

Author SHA1 Message Date
b4707e200f
add option for user to disable MFA (#6) 2024-05-09 15:02:43 +02:00
80775a9cb4
add pico color themes 2024-05-09 14:00:52 +02:00
24 changed files with 161 additions and 30 deletions

View file

@ -12,6 +12,7 @@ func addWebRoutes(f *fiber.App) {
f.Get("/admin/account/", web.HandleAdminAccountGet) f.Get("/admin/account/", web.HandleAdminAccountGet)
f.Get("/admin/account/mfasetup", web.HandleAdminAccountMFASetupGet) f.Get("/admin/account/mfasetup", web.HandleAdminAccountMFASetupGet)
f.Post("/admin/account/mfasetup", web.HandleAdminAccountMFASetupPost) f.Post("/admin/account/mfasetup", web.HandleAdminAccountMFASetupPost)
f.Delete("/admin/account/mfa", web.HandleAdminAccountMFARemove)
f.Get("/admin/login", web.HandleAdminLoginGet) f.Get("/admin/login", web.HandleAdminLoginGet)
f.Post("/admin/login", web.HandleAdminLoginPost) f.Post("/admin/login", web.HandleAdminLoginPost)

View file

@ -3,10 +3,14 @@ package db
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"code.lila.network/adoralaura/go-urlsh/models" "code.lila.network/adoralaura/go-urlsh/models"
) )
// UserHasMFA checks the DB if given models.User has MFA enabled.
// Returns (true, nil) if User has MFA enabled, (false, nil) if not.
// (false, error) if a DB error happened
func UserHasMFA(user models.User) (bool, error) { func UserHasMFA(user models.User) (bool, error) {
numrows, err := models.DB.NewSelect().Model((*models.MFAConfig)(nil)).Where("username = ?", user.UserName).Where("active = ?", true).Count(context.Background()) numrows, err := models.DB.NewSelect().Model((*models.MFAConfig)(nil)).Where("username = ?", user.UserName).Where("active = ?", true).Count(context.Background())
if err != nil { if err != nil {
@ -33,3 +37,30 @@ func ScratchCodeIsUnique(scratchcode string) bool {
} }
return true return true
} }
// RemoveMFAFromDB removes MFA entries for given models.User from the database.
// Returns nil on success, error otherwise.
func RemoveMFAFromDB(user models.User) error {
hasMfa, err := UserHasMFA(user)
if err != nil {
return fmt.Errorf("[RemoveMFAFromDB] Error removing MFA from DB for user %v: %w", user.UserName, err)
}
if !hasMfa {
return nil
}
_, err = models.DB.NewDelete().Model((*models.MFAConfig)(nil)).Where("username = ?", user.UserName).Exec(context.Background())
if err != nil {
log.Println(err.Error())
return fmt.Errorf("[RemoveMFAFromDB] Error removing MFA Config from DB for user %v: %w", user.UserName, err)
}
_, err = models.DB.NewDelete().Model((*models.MFAScratchCode)(nil)).Where("username = ?", user.UserName).Exec(context.Background())
if err != nil {
log.Println(err.Error())
return fmt.Errorf("[RemoveMFAFromDB] Error removing MFA scratch codes from DB for user %v: %w", user.UserName, err)
}
return nil
}

View file

@ -0,0 +1,49 @@
package web
import (
"log"
"net/http"
"code.lila.network/adoralaura/go-urlsh/internal/constants"
"code.lila.network/adoralaura/go-urlsh/internal/db"
"code.lila.network/adoralaura/go-urlsh/internal/misc"
"github.com/gofiber/fiber/v2"
)
// HandleAdminAccountMFARemove is a DELETE endpoint that handles the deletion
// of the logged in users MFA configuration.
//
// Returns HTTP 401 if no valid user cookie, HTTP 400 if no MFA is configured for the user,
// HTTP 500 if a DB error happened or HTTP 204 if the deletion request succeeded.
func HandleAdminAccountMFARemove(c *fiber.Ctx) error {
if !db.IsCookieValid(c.Cookies(constants.LoginCookieName, "")) {
c.Status(http.StatusUnauthorized)
return nil
}
user, err := db.GetUserFromCookie(c.Cookies(constants.LoginCookieName))
if err != nil {
log.Println(err)
return fiber.NewError(fiber.StatusInternalServerError, "500 Internal Server Error")
}
hasMfa, err := db.UserHasMFA(user)
if err != nil {
log.Println(err)
return fiber.NewError(fiber.StatusInternalServerError, "500 Internal Server Error")
}
if !hasMfa {
return misc.New400Error()
}
err = db.RemoveMFAFromDB(user)
if err != nil {
log.Println(err)
return fiber.NewError(fiber.StatusInternalServerError, "500 Internal Server Error")
}
c.Status(fiber.StatusNoContent)
return nil
}

View file

@ -7,12 +7,19 @@
<title>Account - go-urlsh</title> <title>Account - go-urlsh</title>
<meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="/admin/pico.min.css"> <link rel="stylesheet" href="/admin/pico.min.css">
<link rel="stylesheet" href="/admin/pico.colors.min.css">
<link rel="stylesheet" href="/admin/custom.css"> <link rel="stylesheet" href="/admin/custom.css">
<script src="/admin/main.js" defer></script> <script src="/admin/main.js" defer></script>
<style> <style>
h1, h2, h3, h4, h5, h6 { h1,
margin-bottom: 30px; h2,
h3,
h4,
h5,
h6 {
margin-bottom: 30px;
} }
a[role=button] { a[role=button] {
width: fit-content; width: fit-content;
} }
@ -35,7 +42,7 @@
<a href="/admin/apikeys/">API Keys</a> <a href="/admin/apikeys/">API Keys</a>
</li> </li>
<li> <li>
<a href="javascript: void(0)" >Users (coming soon)</a> <a href="javascript: void(0)">Users (coming soon)</a>
</li> </li>
<li> <li>
<a href="javascript: Logout()" style="color: red;">Logout</a> <a href="javascript: Logout()" style="color: red;">Logout</a>
@ -51,16 +58,22 @@
<h2>Security</h2> <h2>Security</h2>
{{if .MFAEnabled}} {{if .MFAEnabled}}
<a role="button" class="secondary">2fa already enabled</a> <a href="#" onclick="HandleMFARemovalRequest()" role="button">disable Two-Factor Authentication</a>
{{else}} {{else}}
<a href="/admin/account/mfasetup" role="button">Setup 2fa</a> <a href="/admin/account/mfasetup" role="button">Setup Two-Factor Authentication</a>
{{end}} {{end}}
<dialog id="dialog-info"> <dialog id="mfa-disable-dialog" style="flex-direction: column;">
<h2 id="dialog-heading"></h2> <h2 id="dialog-heading">⚠️ Are you sure you want to disable two-factor authentication?</h2>
<p id="dialog-text"></p> <p id="dialog-text">Two-factor authentication adds an additional layer of security to your account by requiring
<form method="dialog"> more than just a password to sign in.<br />If you need to change your configuration, you can delete and re-add
<button>Close</button> Two-Factor authentication again.<br /><br />Do you really want to remove your Two-Factor authentication?</p>
</form> <div class="container grid">
<!-- change those two buttons and rework the functions to close and initiate deletion-->
<button id="button-keep" class="pico-background-green-450" onclick="HandleModalClose('mfa-disable-dialog')">No, keep Two-Factor authentication</button>
<button id="button-delete" class="pico-color-red-450 pico-background-red-450" onclick="HandleMFARemoval()">Yes,
remove Two-Factor authentication</button>
</div>
</dialog> </dialog>
</main> </main>
{{template "partials/footer" .}} {{template "partials/footer" .}}

View file

@ -7,6 +7,7 @@
<title>Add new Shortlink - go-urlsh</title> <title>Add new Shortlink - go-urlsh</title>
<meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="/admin/pico.min.css"> <link rel="stylesheet" href="/admin/pico.min.css">
<link rel="stylesheet" href="/admin/pico.colors.min.css">
<link rel="stylesheet" href="/admin/custom.css"> <link rel="stylesheet" href="/admin/custom.css">
<script src="/admin/main.js" defer></script> <script src="/admin/main.js" defer></script>

View file

@ -7,6 +7,7 @@
<title>Edit Shortlink - go-urlsh</title> <title>Edit Shortlink - go-urlsh</title>
<meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="/admin/pico.min.css"> <link rel="stylesheet" href="/admin/pico.min.css">
<link rel="stylesheet" href="/admin/pico.colors.min.css">
<link rel="stylesheet" href="/admin/custom.css"> <link rel="stylesheet" href="/admin/custom.css">
<script src="/admin/main.js" defer></script> <script src="/admin/main.js" defer></script>

View file

@ -7,6 +7,7 @@
<title>Shortlinks - go-urlsh</title> <title>Shortlinks - go-urlsh</title>
<meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="/admin/pico.min.css"> <link rel="stylesheet" href="/admin/pico.min.css">
<link rel="stylesheet" href="/admin/pico.colors.min.css">
<link rel="stylesheet" href="/admin/custom.css"> <link rel="stylesheet" href="/admin/custom.css">
<script src="/admin/main.js" defer></script> <script src="/admin/main.js" defer></script>

View file

@ -7,6 +7,7 @@
<title>Multi Factor Authentication - go-urlsh</title> <title>Multi Factor Authentication - go-urlsh</title>
<meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="/admin/pico.min.css"> <link rel="stylesheet" href="/admin/pico.min.css">
<link rel="stylesheet" href="/admin/pico.colors.min.css">
<script src="/admin/main.js" defer></script> <script src="/admin/main.js" defer></script>
<style> <style>
.hidden { .hidden {

View file

@ -7,6 +7,7 @@
<title>Login - go-urlsh</title> <title>Login - go-urlsh</title>
<meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="/admin/pico.min.css"> <link rel="stylesheet" href="/admin/pico.min.css">
<link rel="stylesheet" href="/admin/pico.colors.min.css">
<style> <style>
</style> </style>

View file

@ -7,6 +7,7 @@
<title>Setup MFA - go-urlsh</title> <title>Setup MFA - go-urlsh</title>
<meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="/admin/pico.min.css"> <link rel="stylesheet" href="/admin/pico.min.css">
<link rel="stylesheet" href="/admin/pico.colors.min.css">
<link rel="stylesheet" href="/admin/custom.css"> <link rel="stylesheet" href="/admin/custom.css">
<script src="/admin/main.js" defer></script> <script src="/admin/main.js" defer></script>
<style> <style>
@ -71,19 +72,19 @@
<input type="submit" value="Submit" class="contrast" style="width: fit-content;"> <input type="submit" value="Submit" class="contrast" style="width: fit-content;">
</div> </div>
</form> </form>
<dialog id="dialog-success" style="flex-direction: column;"> <dialog id="user-dialog" style="flex-direction: column;">
<h2 id="dialog-heading">2FA successfully set up!</h2> <h2 id="dialog-heading">2FA successfully set up!</h2>
<p id="dialog-text">These are your recovery codes, keep them somewhere safe, you'll only see them once:</p> <p id="dialog-text">These are your recovery codes, keep them somewhere safe, you'll only see them once:</p>
<p id="dialog-tokens" class="monospace"></p> <p id="dialog-tokens" class="monospace"></p>
<form method="dialog"> <form method="dialog">
<button onclick="HandleModalClose('/admin/account')">Close</button> <button onclick="HandleModalClose('user-dialog', '/admin/account')">Close</button>
</form> </form>
</dialog> </dialog>
<dialog id="dialog-error" style="flex-direction: column;"> <dialog id="dialog-error" style="flex-direction: column;">
<h2 id="dialog-heading"></h2> <h2 id="dialog-heading"></h2>
<p id="dialog-text"></p> <p id="dialog-text"></p>
<form method="dialog"> <form method="dialog">
<button onclick="HandleModalClose()">Close</button> <button onclick="HandleModalClose('dialog-error')">Close</button>
</form> </form>
</dialog> </dialog>
</main> </main>

View file

@ -7,8 +7,8 @@
<title>Add new Shortlink - go-urlsh</title> <title>Add new Shortlink - go-urlsh</title>
<meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="/admin/pico.min.css"> <link rel="stylesheet" href="/admin/pico.min.css">
<link rel="stylesheet" href="/admin/pico.colors.min.css">
<link rel="stylesheet" href="/admin/custom.css"> <link rel="stylesheet" href="/admin/custom.css">
<script src="/admin/link_add.js" defer></script>
<script src="/admin/misc.js" defer></script> <script src="/admin/misc.js" defer></script>
</head> </head>

View file

@ -7,6 +7,7 @@
<title>Edit Shortlink - go-urlsh</title> <title>Edit Shortlink - go-urlsh</title>
<meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="/admin/pico.min.css"> <link rel="stylesheet" href="/admin/pico.min.css">
<link rel="stylesheet" href="/admin/pico.colors.min.css">
<link rel="stylesheet" href="/admin/custom.css"> <link rel="stylesheet" href="/admin/custom.css">
<script src="/admin/main.js" defer></script> <script src="/admin/main.js" defer></script>

View file

@ -7,6 +7,7 @@
<title>Shortlinks - go-urlsh</title> <title>Shortlinks - go-urlsh</title>
<meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="/admin/pico.min.css"> <link rel="stylesheet" href="/admin/pico.min.css">
<link rel="stylesheet" href="/admin/pico.colors.min.css">
<link rel="stylesheet" href="/admin/custom.css"> <link rel="stylesheet" href="/admin/custom.css">
<script src="/admin/main.js" defer></script> <script src="/admin/main.js" defer></script>

View file

@ -7,6 +7,7 @@
<title>Login - go-urlsh</title> <title>Login - go-urlsh</title>
<meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="/admin/pico.min.css"> <link rel="stylesheet" href="/admin/pico.min.css">
<link rel="stylesheet" href="/admin/pico.colors.min.css">
<style> <style>
</style> </style>

View file

@ -145,6 +145,39 @@ async function HandleMFASetupTokenSubmit() {
} }
} }
async function HandleMFARemovalRequest() {
document.getElementById('mfa-disable-dialog').showModal()
}
async function HandleMFARemoval() {
let keepButton = document.getElementById('button-keep')
let deleteButton = document.getElementById('button-delete')
keepButton.disabled = true
deleteButton.disabled = true
deleteButton.value = 'Please wait...'
deleteButton.setAttribute('aria-busy', 'true')
endpoint = "/admin/account/mfa"
let response = await fetch(endpoint, {
credentials: "include",
mode: "same-origin",
method: "DELETE"
});
if (response.ok) {
document.location = "/admin/account"
} //else {
// TODO
// Handle error condition
//document.cookie = 'gourlsh_mfa_setup=; Max-Age=-1; path=/; domain=' + location.hostname;
//document.getElementById("dialog-heading").textContent = "Error!"
//document.getElementById("dialog-text").textContent = "Something didnt work!"
//document.getElementById('dialog-error').showModal()
//}
}
async function HandleMFALoginTokenPost() { async function HandleMFALoginTokenPost() {
document.getElementById("submit").disabled = true; document.getElementById("submit").disabled = true;
document.getElementById("token").disabled = true; document.getElementById("token").disabled = true;
@ -174,9 +207,8 @@ async function HandleMFALoginTokenPost() {
} }
} }
function HandleModalClose(redir) { function HandleModalClose(id, redir) {
document.getElementById('dialog-success').close(); document.getElementById(id).close();
document.getElementById('dialog-error').close();
if (redir) { if (redir) {
document.location = redir document.location = redir

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

4
web/pico.colors.min.css vendored Normal file

File diff suppressed because one or more lines are too long

7
web/pico.min.css vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.