init - add project files
This commit is contained in:
58
handlers/app/account.go
Normal file
58
handlers/app/account.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"maxwarden/middleware"
|
||||
"maxwarden/users"
|
||||
|
||||
. "maxwarden/ui"
|
||||
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func AccountHandler(w http.ResponseWriter, r *http.Request) {
|
||||
identity := middleware.GetIdentity(r)
|
||||
session := middleware.GetSession(r)
|
||||
|
||||
|
||||
type accountTableItem struct {
|
||||
Property string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
cols := []string {
|
||||
"Property",
|
||||
"Value",
|
||||
}
|
||||
|
||||
user, _ := users.FetchById(identity.UserID)
|
||||
|
||||
entries := []accountTableItem {
|
||||
{ "UserId", user.ID },
|
||||
{ "Username", user.Username },
|
||||
{ "Name", user.Firstname + " " + user.Lastname },
|
||||
{ "Email", user.Email },
|
||||
{ "Last Login", user.LastLogin },
|
||||
}
|
||||
|
||||
func() Node {
|
||||
return AppLayout("Account", *identity, session,
|
||||
AutoTableLite(
|
||||
cols,
|
||||
entries,
|
||||
func(item accountTableItem) Node {
|
||||
return Tr(
|
||||
Td(B(Text(item.Property))),
|
||||
Td(ToText(item.Value)),
|
||||
)
|
||||
},
|
||||
AutoTableOptions{
|
||||
BorderX: true,
|
||||
Shadow: true,
|
||||
},
|
||||
),
|
||||
)
|
||||
}().Render(w)
|
||||
}
|
||||
17
handlers/app/delete.go
Normal file
17
handlers/app/delete.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"maxwarden/entries"
|
||||
"maxwarden/middleware"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func DeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
identity := middleware.GetIdentity(r)
|
||||
|
||||
id := r.PathValue("id")
|
||||
|
||||
entries.DeleteSecret(identity.UserID, identity.MasterKey, id)
|
||||
|
||||
http.Redirect(w, r, "/app", http.StatusFound)
|
||||
}
|
||||
149
handlers/app/editor.go
Normal file
149
handlers/app/editor.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"maxwarden/entries"
|
||||
"maxwarden/security"
|
||||
. "maxwarden/ui"
|
||||
"maxwarden/users"
|
||||
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
|
||||
"maxwarden/middleware"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
EDITOR_TYPE_EDIT = iota
|
||||
EDITOR_TYPE_ADD = iota
|
||||
)
|
||||
|
||||
func EditorHandler(w http.ResponseWriter, r *http.Request) {
|
||||
identity := middleware.GetIdentity(r)
|
||||
session := middleware.GetSession(r)
|
||||
|
||||
var editorType int
|
||||
var title string
|
||||
var btnLabel string
|
||||
|
||||
if r.URL.Path == "/app/editor/add" {
|
||||
editorType = EDITOR_TYPE_ADD
|
||||
title = "Add Credentials"
|
||||
btnLabel = "Add"
|
||||
} else {
|
||||
editorType = EDITOR_TYPE_EDIT
|
||||
title = "Edit Credentials"
|
||||
btnLabel = "Save"
|
||||
}
|
||||
|
||||
var secret entries.Secret
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
if editorType == EDITOR_TYPE_EDIT {
|
||||
id := r.PathValue("id")
|
||||
|
||||
secret, _ = entries.FetchSecretFromID(identity.UserID, identity.MasterKey, id)
|
||||
}
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
r.ParseForm()
|
||||
|
||||
desc := r.FormValue("description")
|
||||
notes := r.FormValue("notes")
|
||||
username := r.FormValue("un")
|
||||
password := r.FormValue("pas")
|
||||
url := r.FormValue("url")
|
||||
|
||||
secret = entries.Secret{
|
||||
Description: desc,
|
||||
URL: url,
|
||||
Notes: notes,
|
||||
Password: password,
|
||||
Username: username,
|
||||
}
|
||||
|
||||
user, _ := users.FetchById(identity.UserID)
|
||||
|
||||
// Get current secret store
|
||||
secrets, _ := security.DecryptDataWithKey[[]entries.Secret](user.Data, identity.MasterKey)
|
||||
|
||||
if secrets == nil {
|
||||
http.Redirect(w, r, "/app", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
if editorType == EDITOR_TYPE_ADD {
|
||||
secret.ID = security.RandBase58String(32)
|
||||
*secrets = append(*secrets, secret)
|
||||
} else {
|
||||
secret.ID = r.PathValue("id")
|
||||
|
||||
// linear search and replace
|
||||
for i, v := range *secrets {
|
||||
if v.ID == secret.ID {
|
||||
(*secrets)[i] = secret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize and encrypt modified store using master key
|
||||
enc, _ := security.EncryptDataWithKey(secrets, identity.MasterKey)
|
||||
|
||||
user.Data = enc
|
||||
|
||||
users.Update(user)
|
||||
|
||||
http.Redirect(w, r, "/app", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
AppLayout(title, *identity, session,
|
||||
If(editorType == EDITOR_TYPE_EDIT,
|
||||
Group{
|
||||
Modal(
|
||||
"warning_popup",
|
||||
Text("Warning!"),
|
||||
Text("Are you sure you want to delete this entry? This action cannot be undone."),
|
||||
[]Node{
|
||||
A(Href("/app/delete/" + secret.ID), ButtonUIDanger(Text("Delete"))),
|
||||
ButtonUIOutline(ModalCloser(), Text("Close")),
|
||||
},
|
||||
),
|
||||
Div(
|
||||
InlineStyle("$me { display: flex; flex-direction: row-reverse; align-items: center; }"),
|
||||
ModalActuator("warning_popup", ButtonUIDanger(Text("Delete"))),
|
||||
),
|
||||
},
|
||||
),
|
||||
Form(
|
||||
AutoComplete("off"),
|
||||
Method("POST"),
|
||||
FormLabel(Text("Description")),
|
||||
FormInput(Type("text"), Name("description"), Value(secret.Description)),
|
||||
Br(),
|
||||
|
||||
FormLabel(Text("Username")),
|
||||
FormInput(Type("text"), Name("un"), Value(secret.Username)),
|
||||
Br(),
|
||||
|
||||
FormLabel(Text("Password")),
|
||||
FormInput(Type("password"), Name("pas"), Value(secret.Password)),
|
||||
Br(),
|
||||
|
||||
FormLabel(Text("URL")),
|
||||
FormInput(Type("text"), Name("url"), Value(secret.URL)),
|
||||
Br(),
|
||||
|
||||
FormLabel(Text("Additional Notes")),
|
||||
FormTextarea(InlineStyle("$me { height: $32; font-family: var(--font-mono); }"), Name("notes"), Text(secret.Notes)),
|
||||
Br(),
|
||||
|
||||
Div(
|
||||
InlineStyle("$me { display: flex; flex-direction: row; align-items: center; gap: $4; }"),
|
||||
ButtonUISuccess(Text(btnLabel), Type("submit")),
|
||||
A(Href("/app"), ButtonUIOutline(Text("Close"), Type("button"))),
|
||||
),
|
||||
),
|
||||
).Render(w)
|
||||
}
|
||||
32
handlers/app/vault.go
Normal file
32
handlers/app/vault.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
. "maxwarden/ui"
|
||||
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
|
||||
"maxwarden/middleware"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func VaultHandler(w http.ResponseWriter, r *http.Request) {
|
||||
identity := middleware.GetIdentity(r)
|
||||
session := middleware.GetSession(r)
|
||||
|
||||
AppLayout("Credential Vault", *identity, session,
|
||||
Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
margin-bottom: $5;
|
||||
}
|
||||
`),
|
||||
A(Href("/app/editor/add"), ButtonUI(Text("+ Add Item"))),
|
||||
),
|
||||
HxLoad("/app/vault-hx"),
|
||||
).Render(w)
|
||||
}
|
||||
91
handlers/app/vault_hx.go
Normal file
91
handlers/app/vault_hx.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
// "maxwarden/.jet/table"
|
||||
|
||||
"maxwarden/security"
|
||||
. "maxwarden/ui"
|
||||
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
|
||||
"maxwarden/database"
|
||||
"maxwarden/entries"
|
||||
|
||||
"maxwarden/middleware"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func VaultHxHandler(w http.ResponseWriter, r *http.Request) {
|
||||
identity := middleware.GetIdentity(r)
|
||||
|
||||
filter := database.ParseFilterFromRequest(r)
|
||||
filter.Pagination.Enabled = true
|
||||
|
||||
entryFilter := entries.EntryFilter{
|
||||
Filter: filter,
|
||||
UserId: identity.UserID,
|
||||
MasterKey: identity.MasterKey,
|
||||
}
|
||||
|
||||
// fetch entities from filter function
|
||||
// this first counts the possible items before pagination
|
||||
searchFilter := entries.EntryFilter{
|
||||
Filter: database.NewFilterFromSearch(filter.Search),
|
||||
UserId: identity.UserID,
|
||||
MasterKey: identity.MasterKey,
|
||||
}
|
||||
|
||||
searchItems, _ := entries.Filter(searchFilter)
|
||||
|
||||
// this query gets the data AFTER pagination
|
||||
entryList, _ := entries.Filter(entryFilter)
|
||||
|
||||
// generate page numbers according to total length of data
|
||||
entryFilter.Filter.Pagination.GeneratePagination(len(searchItems), len(entryList))
|
||||
|
||||
// Col header names and referenced database col names
|
||||
cols := []database.ColInfo{
|
||||
{DbName: "Description", DisplayName: "Description", Sortable: true},
|
||||
{DisplayName: "Username"},
|
||||
{DisplayName: "Password"},
|
||||
{DisplayName: "URL"},
|
||||
{DisplayName: "Action"},
|
||||
}
|
||||
|
||||
// Generate HTML
|
||||
elId := "order_table"
|
||||
AutoTable(
|
||||
elId,
|
||||
r.URL.Path,
|
||||
cols,
|
||||
entryFilter.Filter,
|
||||
entryList,
|
||||
AutotableSearchGroup(
|
||||
AutotableSearch(
|
||||
Placeholder("Search Description..."),
|
||||
BindSearch(elId, "description"),
|
||||
AutoFocus(),
|
||||
),
|
||||
),
|
||||
func(entry entries.Secret) Node {
|
||||
return Tr(
|
||||
TdLeft(Text(entry.Description)),
|
||||
TdLeft(Text(entry.Username)),
|
||||
TdLeft(Text("********")),
|
||||
TdLeft(PageLink(security.SanitizationPolicy.Sanitize(entry.URL), Text(entry.URL), false)),
|
||||
TdCenter(A(Href("/app/editor/edit/" + entry.ID), ButtonUIOutline(Icon(ICON_PENCIL, 16)))),
|
||||
)
|
||||
},
|
||||
nil,
|
||||
AutoTableOptions{
|
||||
Compact: false,
|
||||
Shadow: true,
|
||||
Hover: false,
|
||||
Alternate: false,
|
||||
BorderX: true,
|
||||
BorderY: false,
|
||||
},
|
||||
).Render(w)
|
||||
}
|
||||
218
handlers/auth/login.go
Normal file
218
handlers/auth/login.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
. "maxwarden/ui"
|
||||
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
|
||||
"maxwarden/auth"
|
||||
"maxwarden/config"
|
||||
"maxwarden/middleware"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
LoginView("").Render(w)
|
||||
} else if r.Method == http.MethodPost {
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
userid, securityStamp, authResult := auth.Authenticate(username, password)
|
||||
|
||||
if !authResult {
|
||||
log.Println("Failed login attempt. Username: " + username)
|
||||
LoginView("Username or password incorrect.").Render(w)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Successful login. Username: " + username)
|
||||
|
||||
// build identity info
|
||||
identity := auth.NewIdentity(userid, securityStamp, password, false)
|
||||
|
||||
// serialize and send as cookie
|
||||
middleware.PutIdentityCookie(w, r, identity)
|
||||
|
||||
params := r.URL.Query()
|
||||
location := params.Get("redirect")
|
||||
|
||||
if len(params["redirect"]) > 0 {
|
||||
http.Redirect(w, r, location, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
defaultPath := config.IDENTITY_DEFAULT_PATH
|
||||
http.Redirect(w, r, defaultPath, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func LoginView(errorMsg string) Node {
|
||||
currentYear := time.Time.Year(time.Now())
|
||||
|
||||
return RootLayout("Login",
|
||||
Body(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
height: 100%;
|
||||
background: $color(light-grey);
|
||||
}
|
||||
`),
|
||||
Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: normal;
|
||||
padding-right: $6;
|
||||
padding-left: $6;
|
||||
padding-bottom: $5;
|
||||
padding-top: $24;
|
||||
}
|
||||
|
||||
@media $md {
|
||||
$me {
|
||||
padding-right: $8;
|
||||
padding-left: $8;
|
||||
}
|
||||
}
|
||||
`),
|
||||
Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
margin-bottom: $3;
|
||||
}
|
||||
|
||||
@media $sm {
|
||||
$me {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
width: 100%;
|
||||
max-width: var(--container-sm);
|
||||
}
|
||||
}
|
||||
`),
|
||||
A(Href("/"),
|
||||
Img(
|
||||
InlineStyle("$me { margin-right: auto; margin-left: auto; height: $32; width: auto; }"),
|
||||
Src("/images/logo.png"),
|
||||
Alt("MaxWarden"),
|
||||
),
|
||||
),
|
||||
),
|
||||
Div(
|
||||
InlineStyle(`
|
||||
@media $sm {
|
||||
$me {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
width: 100%;
|
||||
max-width: var(--container-sm);
|
||||
}
|
||||
}
|
||||
`),
|
||||
If(errorMsg != "",
|
||||
Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
margin-top: $5;
|
||||
}
|
||||
|
||||
@media $sm {
|
||||
$me {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
width: 100%;
|
||||
max-width: var(--container-sm);
|
||||
}
|
||||
}
|
||||
`),
|
||||
P(InlineStyle("$me { font-size: var(--text-sm); color: $color(red-500); }"), Text(errorMsg)),
|
||||
),
|
||||
),
|
||||
Form(InlineStyle("$me { margin-top: $5; }"), Action(""), Method("POST"), AutoComplete("off"),
|
||||
H2(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
margin-top: $10;
|
||||
margin-bottom: $5;
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--text-2xl);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
color: $color(deep-blue);
|
||||
}
|
||||
`),
|
||||
Text("Secure Sign In"),
|
||||
),
|
||||
Div(
|
||||
Label(
|
||||
InlineStyle("$me { display: block; font-size: var(--text-xs); font-weight: var(--font-weight-normal); color: $color(neutral-700);}"),
|
||||
For("username"),
|
||||
Text("Username"),
|
||||
),
|
||||
Div(InlineStyle("$me { margin-top: $2; }"),
|
||||
FormInput(Name("username"), Type("text"), Required()),
|
||||
),
|
||||
),
|
||||
Div(
|
||||
Label(
|
||||
InlineStyle("$me { margin-top: $5; display: block; font-size: var(--text-xs); font-weight: var(--font-weight-normal); color: $color(neutral-700);}"),
|
||||
For("password"),
|
||||
Text("Master Password"),
|
||||
),
|
||||
Div(InlineStyle("$me { margin-top: $2; }"),
|
||||
FormInput(Name("password"), Type("password"), Required()),
|
||||
),
|
||||
),
|
||||
Div(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
margin-top: $5;
|
||||
}
|
||||
`),
|
||||
Button(
|
||||
InlineStyle(`
|
||||
$me {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding-top: $2;
|
||||
padding-bottom: $2;
|
||||
padding-left: $5;
|
||||
padding-right: $5;
|
||||
color: $color(white);
|
||||
background-color: $color(deep-blue);
|
||||
border-radius: var(--radius-xs);
|
||||
text-align: center;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
$me:hover {
|
||||
background-color: $color(indigo-blue);
|
||||
}
|
||||
`),
|
||||
Type("submit"),
|
||||
Text("Sign In"),
|
||||
),
|
||||
),
|
||||
InlineScript(`
|
||||
let form = me();
|
||||
let btn = me("button", me());
|
||||
|
||||
form.on("submit", () => { btn.innerHTML = "Authenticating..."; });
|
||||
`),
|
||||
),
|
||||
P(
|
||||
InlineStyle("$me { margin-top: $10; font-size: var(--text-sm); color: $color(neutral-500);}"),
|
||||
Text("© "),
|
||||
ToText(currentYear),
|
||||
Text(" Max Amundsen"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
14
handlers/auth/logout.go
Normal file
14
handlers/auth/logout.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"maxwarden/config"
|
||||
"maxwarden/middleware"
|
||||
)
|
||||
|
||||
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.DeleteIdentityCookie(w, r)
|
||||
middleware.DeleteSessionCookie(w, r)
|
||||
|
||||
http.Redirect(w, r, config.IDENTITY_LOGIN_PATH, http.StatusFound)
|
||||
}
|
||||
37
handlers/index.go
Normal file
37
handlers/index.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
. "maxwarden/ui"
|
||||
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// serve home page if route is literally '/'
|
||||
if r.URL.Path == "/" {
|
||||
http.Redirect(w, r, "/auth/login", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
// By default, any unmapped route will route to '/', so make sure
|
||||
// the URL is actually '/' or else 404
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
||||
ErrorPage(http.StatusNotFound).Render(w)
|
||||
return
|
||||
}
|
||||
|
||||
rr := &httptest.ResponseRecorder{Code: http.StatusOK}
|
||||
|
||||
HttpFS.ServeHTTP(rr, r)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
w.WriteHeader(rr.Code)
|
||||
ErrorPage(rr.Code).Render(w)
|
||||
} else {
|
||||
HttpFS.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
9
handlers/init.go
Normal file
9
handlers/init.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package handlers
|
||||
|
||||
import "net/http"
|
||||
|
||||
var HttpFS http.Handler
|
||||
|
||||
func Init() {
|
||||
HttpFS = http.FileServer(http.Dir("wwwroot"))
|
||||
}
|
||||
Reference in New Issue
Block a user