Compare commits
2 commits
a019e55c74
...
b4707e200f
Author | SHA1 | Date | |
---|---|---|---|
b4707e200f | |||
80775a9cb4 |
24 changed files with 161 additions and 30 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
49
internal/web/multifactor-remove.go
Normal file
49
internal/web/multifactor-remove.go
Normal 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
|
||||
}
|
|
@ -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 {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
a[role=button] {
|
||||
width: fit-content;
|
||||
}
|
||||
|
@ -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>
|
||||
<a href="/admin/account/mfasetup" role="button">Setup Two-Factor Authentication</a>
|
||||
{{end}}
|
||||
<dialog id="dialog-info">
|
||||
<h2 id="dialog-heading"></h2>
|
||||
<p id="dialog-text"></p>
|
||||
<form method="dialog">
|
||||
<button>Close</button>
|
||||
</form>
|
||||
<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" .}}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
38
web/main.js
38
web/main.js
|
@ -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
|
||||
|
|
5
web/pico.classless.min.css
vendored
5
web/pico.classless.min.css
vendored
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
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
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.
Loading…
Reference in a new issue