TOTP fully functioning, account settings done!

This commit is contained in:
ari melody 2025-01-23 13:53:06 +00:00
parent 50cbce92fc
commit e004491b55
Signed by: ari
GPG key ID: CF99829C92678188
11 changed files with 143 additions and 48 deletions

View file

@ -30,15 +30,30 @@ func accountIndexHandler(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
totps, err := controller.GetTOTPsForAccount(app.DB, session.Account.ID) dbTOTPs, err := controller.GetTOTPsForAccount(app.DB, session.Account.ID)
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to fetch TOTPs: %v\n", err) fmt.Printf("WARN: Failed to fetch TOTPs: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} }
type accountResponse struct { type (
Session *model.Session TOTP struct {
TOTPs []model.TOTP model.TOTP
CreatedAtString string
}
accountResponse struct {
Session *model.Session
TOTPs []TOTP
}
)
totps := []TOTP{}
for _, totp := range dbTOTPs {
totps = append(totps, TOTP{
TOTP: totp,
CreatedAtString: totp.CreatedAt.Format("02 Jan 2006, 15:04:05"),
})
} }
sessionMessage := session.Message sessionMessage := session.Message

View file

@ -284,26 +284,65 @@ func loginHandler(app *model.AppState) http.Handler {
return return
} }
totpMethod, err := controller.CheckTOTPForAccount(app.DB, account.ID, credentials.TOTP) var totpMethod *model.TOTP
if err != nil { if len(credentials.TOTP) == 0 {
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch TOTPs: %v\n", err) // check if user has TOTP
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") totps, err := controller.GetTOTPsForAccount(app.DB, account.ID)
render() if err != nil {
return fmt.Fprintf(os.Stderr, "WARN: Failed to fetch TOTPs: %v\n", err)
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
render()
return
}
if len(totps) > 0 {
type loginTOTPData struct {
Session *model.Session
Username string
Password string
}
err = pages["login-totp"].Execute(w, loginTOTPData{
Session: session,
Username: credentials.Username,
Password: credentials.Password,
})
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to render login TOTP page: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
} else {
totpMethod, err = controller.CheckTOTPForAccount(app.DB, account.ID, credentials.TOTP)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch TOTPs: %v\n", err)
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
render()
return
}
if totpMethod == nil {
controller.SetSessionError(app.DB, session, "Invalid TOTP.")
render()
return
}
} }
if totpMethod == nil {
controller.SetSessionError(app.DB, session, "Invalid TOTP.") if totpMethod != nil {
render() fmt.Printf(
return "[%s] INFO: Account \"%s\" logged in with method \"%s\"\n",
time.Now().Format(time.UnixDate),
account.Username,
totpMethod.Name,
)
} else {
fmt.Printf(
"[%s] INFO: Account \"%s\" logged in\n",
time.Now().Format(time.UnixDate),
account.Username,
)
} }
// TODO: log login activity to user // TODO: log login activity to user
fmt.Printf(
"[%s] INFO: Account \"%s\" logged in with method \"%s\"\n",
time.Now().Format(time.UnixDate),
account.Username,
totpMethod.Name,
)
// login success! // login success!
controller.SetSessionAccount(app.DB, session, account) controller.SetSessionAccount(app.DB, session, account)

View file

@ -85,6 +85,15 @@ a img.icon {
height: .8em; height: .8em;
} }
code {
background: #303030;
color: #f0f0f0;
padding: .23em .3em;
border-radius: 4px;
}
.card { .card {
margin-bottom: 1em; margin-bottom: 1em;
} }
@ -93,13 +102,6 @@ a img.icon {
margin: 0 0 .5em 0; margin: 0 0 .5em 0;
} }
/*
.card h3,
.card p {
margin: 0;
}
*/
.card-title { .card-title {
margin-bottom: 1em; margin-bottom: 1em;
display: flex; display: flex;

View file

@ -18,6 +18,11 @@ var pages = map[string]*template.Template{
filepath.Join("views", "prideflag.html"), filepath.Join("views", "prideflag.html"),
filepath.Join("admin", "views", "login.html"), filepath.Join("admin", "views", "login.html"),
)), )),
"login-totp": template.Must(template.ParseFiles(
filepath.Join("admin", "views", "layout.html"),
filepath.Join("views", "prideflag.html"),
filepath.Join("admin", "views", "login-totp.html"),
)),
"register": template.Must(template.ParseFiles( "register": template.Must(template.ParseFiles(
filepath.Join("admin", "views", "layout.html"), filepath.Join("admin", "views", "layout.html"),
filepath.Join("views", "prideflag.html"), filepath.Join("views", "prideflag.html"),

View file

@ -40,11 +40,11 @@
{{range .TOTPs}} {{range .TOTPs}}
<div class="mfa-device"> <div class="mfa-device">
<div> <div>
<p class="mfa-device-name">{{.Name}}</p> <p class="mfa-device-name">{{.TOTP.Name}}</p>
<p class="mfa-device-date">Added: {{.CreatedAt}}</p> <p class="mfa-device-date">Added: {{.CreatedAtString}}</p>
</div> </div>
<div> <div>
<a class="button delete" href="/admin/account/totp-delete/{{.Name}}">Delete</a> <a class="button delete" href="/admin/account/totp-delete/{{.TOTP.Name}}">Delete</a>
</div> </div>
</div> </div>
{{end}} {{end}}

View file

@ -0,0 +1,42 @@
{{define "head"}}
<title>Login - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/admin.css">
<style>
form#login-totp {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
form div {
width: 20rem;
}
form button {
margin-top: 1rem;
}
input {
width: calc(100% - 1rem - 2px);
}
</style>
{{end}}
{{define "content"}}
<main>
<form action="/admin/login" method="POST" id="login-totp">
<h1>Two-Factor Authentication</h1>
<div>
<label for="totp">TOTP</label>
<input type="text" name="totp" value="" autocomplete="one-time-code" required autofocus>
<input type="hidden" name="username" value="{{.Username}}">
<input type="hidden" name="password" value="{{.Password}}">
</div>
<button type="submit" class="save">Login</button>
</form>
</main>
{{end}}

View file

@ -3,14 +3,6 @@
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/admin.css"> <link rel="stylesheet" href="/admin/static/admin.css">
<style> <style>
p a {
color: #2a67c8;
}
a.discord {
color: #5865F2;
}
form#login { form#login {
width: 100%; width: 100%;
display: flex; display: flex;
@ -27,7 +19,7 @@ form button {
} }
input { input {
width: 100%; width: calc(100% - 1rem - 2px);
} }
</style> </style>
{{end}} {{end}}
@ -42,15 +34,14 @@ input {
{{end}} {{end}}
<form action="/admin/login" method="POST" id="login"> <form action="/admin/login" method="POST" id="login">
<h1>Log In</h1>
<div> <div>
<label for="username">Username</label> <label for="username">Username</label>
<input type="text" name="username" value="" autocomplete="username" required> <input type="text" name="username" value="" autocomplete="username" required autofocus>
<label for="password">Password</label> <label for="password">Password</label>
<input type="password" name="password" value="" autocomplete="current-password" required> <input type="password" name="password" value="" autocomplete="current-password" required>
<label for="totp">TOTP</label>
<input type="text" name="totp" value="" autocomplete="one-time-code" required>
</div> </div>
<button type="submit" class="save">Login</button> <button type="submit" class="save">Login</button>

View file

@ -27,7 +27,7 @@ form button {
} }
input { input {
width: 100%; width: calc(100% - 1rem - 2px);
} }
</style> </style>
{{end}} {{end}}
@ -39,9 +39,11 @@ input {
{{end}} {{end}}
<form action="/admin/register" method="POST" id="register"> <form action="/admin/register" method="POST" id="register">
<h1>Create Account</h1>
<div> <div>
<label for="username">Username</label> <label for="username">Username</label>
<input type="text" name="username" value="" autocomplete="username" required> <input type="text" name="username" value="" autocomplete="username" required autofocus>
<label for="email">Email</label> <label for="email">Email</label>
<input type="text" name="email" value="" autocomplete="email" required> <input type="text" name="email" value="" autocomplete="email" required>

View file

@ -26,7 +26,7 @@ code {
</p> </p>
<label for="totp">TOTP:</label> <label for="totp">TOTP:</label>
<input type="text" name="totp" value="" autocomplete="one-time-code" required> <input type="text" name="totp" value="" autocomplete="one-time-code" required autofocus>
<button type="submit" class="new">Create</button> <button type="submit" class="new">Create</button>
</form> </form>

View file

@ -12,7 +12,7 @@
<form action="/admin/account/totp-setup" method="POST" id="totp-setup"> <form action="/admin/account/totp-setup" method="POST" id="totp-setup">
<label for="totp-name">TOTP Device Name:</label> <label for="totp-name">TOTP Device Name:</label>
<input type="text" name="totp-name" value="" autocomplete="off" required> <input type="text" name="totp-name" value="" autocomplete="off" required autofocus>
<button type="submit" class="new">Create</button> <button type="submit" class="new">Create</button>
</form> </form>

View file

@ -92,7 +92,6 @@ func GetTOTPsForAccount(db *sqlx.DB, accountID string) ([]model.TOTP, error) {
func CheckTOTPForAccount(db *sqlx.DB, accountID string, totp string) (*model.TOTP, error) { func CheckTOTPForAccount(db *sqlx.DB, accountID string, totp string) (*model.TOTP, error) {
totps, err := GetTOTPsForAccount(db, accountID) totps, err := GetTOTPsForAccount(db, accountID)
if err != nil { if err != nil {
// user has no TOTP methods
return nil, err return nil, err
} }
for _, method := range totps { for _, method := range totps {