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/mfasetup", web.HandleAdminAccountMFASetupGet)
f.Post("/admin/account/mfasetup", web.HandleAdminAccountMFASetupPost)
f.Delete("/admin/account/mfa", web.HandleAdminAccountMFARemove)
f.Get("/admin/login", web.HandleAdminLoginGet)
f.Post("/admin/login", web.HandleAdminLoginPost)

View file

@ -3,10 +3,14 @@ package db
import (
"context"
"fmt"
"log"
"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) {
numrows, err := models.DB.NewSelect().Model((*models.MFAConfig)(nil)).Where("username = ?", user.UserName).Where("active = ?", true).Count(context.Background())
if err != nil {
@ -33,3 +37,30 @@ func ScratchCodeIsUnique(scratchcode string) bool {
}
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>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="/admin/pico.min.css">
<link rel="stylesheet" href="/admin/pico.colors.min.css">
<link rel="stylesheet" href="/admin/custom.css">
<script src="/admin/main.js" defer></script>
<style>
h1, h2, h3, h4, h5, h6 {
margin-bottom: 30px;
h1,
h2,
h3,
h4,
h5,
h6 {
margin-bottom: 30px;
}
a[role=button] {
width: fit-content;
}
@ -35,7 +42,7 @@
<a href="/admin/apikeys/">API Keys</a>
</li>
<li>
<a href="javascript: void(0)" >Users (coming soon)</a>
<a href="javascript: void(0)">Users (coming soon)</a>
</li>
<li>
<a href="javascript: Logout()" style="color: red;">Logout</a>
@ -51,16 +58,22 @@
<h2>Security</h2>
{{if .MFAEnabled}}
<a role="button" class="secondary">2fa already enabled</a>
<a href="#" onclick="HandleMFARemovalRequest()" role="button">disable Two-Factor Authentication</a>
{{else}}
<a href="/admin/account/mfasetup" role="button">Setup 2fa</a>
{{end}}
<dialog id="dialog-info">
<h2 id="dialog-heading"></h2>
<p id="dialog-text"></p>
<form method="dialog">
<button>Close</button>
</form>
<a href="/admin/account/mfasetup" role="button">Setup Two-Factor Authentication</a>
{{end}}
<dialog id="mfa-disable-dialog" style="flex-direction: column;">
<h2 id="dialog-heading">⚠️ Are you sure you want to disable two-factor authentication?</h2>
<p id="dialog-text">Two-factor authentication adds an additional layer of security to your account by requiring
more than just a password to sign in.<br />If you need to change your configuration, you can delete and re-add
Two-Factor authentication again.<br /><br />Do you really want to remove your Two-Factor authentication?</p>
<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>
</main>
{{template "partials/footer" .}}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@
<title>Setup MFA - go-urlsh</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="/admin/pico.min.css">
<link rel="stylesheet" href="/admin/pico.colors.min.css">
<link rel="stylesheet" href="/admin/custom.css">
<script src="/admin/main.js" defer></script>
<style>
@ -71,19 +72,19 @@
<input type="submit" value="Submit" class="contrast" style="width: fit-content;">
</div>
</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>
<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>
<form method="dialog">
<button onclick="HandleModalClose('/admin/account')">Close</button>
<button onclick="HandleModalClose('user-dialog', '/admin/account')">Close</button>
</form>
</dialog>
<dialog id="dialog-error" style="flex-direction: column;">
<h2 id="dialog-heading"></h2>
<p id="dialog-text"></p>
<form method="dialog">
<button onclick="HandleModalClose()">Close</button>
<button onclick="HandleModalClose('dialog-error')">Close</button>
</form>
</dialog>
</main>

View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@
<title>Login - go-urlsh</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="/admin/pico.min.css">
<link rel="stylesheet" href="/admin/pico.colors.min.css">
<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() {
document.getElementById("submit").disabled = true;
document.getElementById("token").disabled = true;
@ -174,9 +207,8 @@ async function HandleMFALoginTokenPost() {
}
}
function HandleModalClose(redir) {
document.getElementById('dialog-success').close();
document.getElementById('dialog-error').close();
function HandleModalClose(id, redir) {
document.getElementById(id).close();
if (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.