init - add project files

This commit is contained in:
2025-03-06 23:54:11 -05:00
commit e724ff1120
1363 changed files with 897467 additions and 0 deletions

58
handlers/app/account.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,9 @@
package handlers
import "net/http"
var HttpFS http.Handler
func Init() {
HttpFS = http.FileServer(http.Dir("wwwroot"))
}