summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth <elizabeth@simponic.xyz>2024-04-09 18:39:14 -0400
committersimponic <simponic@hatecomputers.club>2024-04-09 18:39:14 -0400
commit1d75bf7489527925217bd5611ba7910c0ffe077c (patch)
tree3b6e6056912648a88e1e42c1e42ed7e58e2d4701
parentee49015cc90e6c136ad94243fffc9241b9506a36 (diff)
downloadhatecomputers.club-1d75bf7489527925217bd5611ba7910c0ffe077c.tar.gz
hatecomputers.club-1d75bf7489527925217bd5611ba7910c0ffe077c.zip
profiles (#7)
Reviewed-on: https://git.hatecomputers.club/hatecomputers/hatecomputers.club/pulls/7 Co-authored-by: Elizabeth <elizabeth@simponic.xyz> Co-committed-by: Elizabeth <elizabeth@simponic.xyz>
-rw-r--r--.gitignore1
-rw-r--r--Dockerfile2
-rw-r--r--adapters/external_dns/cloudflare/cloudflare.go (renamed from adapters/cloudflare/cloudflare.go)0
-rw-r--r--adapters/external_dns/external_dns.go (renamed from adapters/external_dns.go)0
-rw-r--r--adapters/files/files_adapter.go8
-rw-r--r--adapters/files/filesystem/filesystem.go37
-rw-r--r--api/auth/auth.go15
-rw-r--r--api/dns/dns.go88
-rw-r--r--api/dns/dns_test.go4
-rw-r--r--api/guestbook/guestbook.go66
-rw-r--r--api/hcaptcha/hcaptcha.go4
-rw-r--r--api/keys/keys.go20
-rw-r--r--api/profiles/profiles.go118
-rw-r--r--api/serve.go36
-rw-r--r--api/types/types.go4
-rw-r--r--args/args.go9
-rw-r--r--database/migrate.go36
-rw-r--r--database/users.go47
-rw-r--r--docker-compose.yml1
-rw-r--r--hcdns/server_test.go2
-rw-r--r--static/css/club.css48
-rw-r--r--static/css/colors.css11
-rw-r--r--static/css/form.css3
-rw-r--r--static/css/guestbook.css1
-rw-r--r--static/css/styles.css36
-rw-r--r--static/img/default-avatar.pngbin0 -> 105755 bytes
-rw-r--r--static/js/components/infoBanners.js6
-rw-r--r--static/js/script.js1
-rw-r--r--templates/api_keys.html7
-rw-r--r--templates/base.html15
-rw-r--r--templates/dns.html14
-rw-r--r--templates/guestbook.html9
-rw-r--r--templates/home.html18
-rw-r--r--templates/profile.html24
-rw-r--r--utils/random_id.go (renamed from utils/RandomId.go)0
35 files changed, 558 insertions, 133 deletions
diff --git a/.gitignore b/.gitignore
index c7bbdba..12a6077 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.env
hatecomputers.club
*.db
+uploads
diff --git a/Dockerfile b/Dockerfile
index 790c580..a95f9b3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,4 +11,4 @@ RUN go build -o /app/hatecomputers
EXPOSE 8080
-CMD ["/app/hatecomputers", "--server", "--migrate", "--port", "8080", "--template-path", "/app/templates", "--database-path", "/app/db/hatecomputers.db", "--static-path", "/app/static", "--scheduler", "--dns", "--dns-port", "8053", "--dns-resolvers", "1.1.1.1:53,1.0.0.1:53"]
+CMD ["/app/hatecomputers", "--server", "--migrate", "--port", "8080", "--template-path", "/app/templates", "--database-path", "/app/db/hatecomputers.db", "--static-path", "/app/static", "--scheduler", "--dns", "--dns-port", "8053", "--dns-resolvers", "1.1.1.1:53,1.0.0.1:53", "--uploads", "/app/uploads"]
diff --git a/adapters/cloudflare/cloudflare.go b/adapters/external_dns/cloudflare/cloudflare.go
index c302037..c302037 100644
--- a/adapters/cloudflare/cloudflare.go
+++ b/adapters/external_dns/cloudflare/cloudflare.go
diff --git a/adapters/external_dns.go b/adapters/external_dns/external_dns.go
index c861283..c861283 100644
--- a/adapters/external_dns.go
+++ b/adapters/external_dns/external_dns.go
diff --git a/adapters/files/files_adapter.go b/adapters/files/files_adapter.go
new file mode 100644
index 0000000..bf3ea5f
--- /dev/null
+++ b/adapters/files/files_adapter.go
@@ -0,0 +1,8 @@
+package files
+
+import "io"
+
+type FilesAdapter interface {
+ CreateFile(path string, content io.Reader) (string, error)
+ DeleteFile(path string) error
+}
diff --git a/adapters/files/filesystem/filesystem.go b/adapters/files/filesystem/filesystem.go
new file mode 100644
index 0000000..726a588
--- /dev/null
+++ b/adapters/files/filesystem/filesystem.go
@@ -0,0 +1,37 @@
+package filesystem
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+)
+
+type FilesystemAdapter struct {
+ BasePath string
+ Permissions os.FileMode
+}
+
+func (f *FilesystemAdapter) CreateFile(path string, content io.Reader) (string, error) {
+ fullPath := f.BasePath + path
+ dir := filepath.Dir(fullPath)
+ if _, err := os.Stat(dir); os.IsNotExist(err) {
+ os.MkdirAll(dir, f.Permissions)
+ }
+
+ file, err := os.Create(f.BasePath + path)
+ if err != nil {
+ return "", err
+ }
+ defer file.Close()
+
+ _, err = io.Copy(file, content)
+ if err != nil {
+ return "", err
+ }
+
+ return path, nil
+}
+
+func (f *FilesystemAdapter) DeleteFile(path string) error {
+ return os.Remove(f.BasePath + path)
+}
diff --git a/api/auth/auth.go b/api/auth/auth.go
index 0ffbf9c..04d6c12 100644
--- a/api/auth/auth.go
+++ b/api/auth/auth.go
@@ -18,6 +18,18 @@ import (
"golang.org/x/oauth2"
)
+func ListUsersContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
+ users, err := database.ListUsers(context.DBConn)
+ if err != nil {
+ return failure(context, req, resp)
+ }
+
+ (*context.TemplateData)["Users"] = users
+ return success(context, req, resp)
+ }
+}
+
func StartSessionContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
verifier := utils.RandomId() + utils.RandomId()
@@ -158,6 +170,7 @@ func VerifySessionContinuation(context *types.RequestContext, req *http.Request,
func GoLoginContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
+ log.Println("GoLoginContinuation")
http.SetCookie(resp, &http.Cookie{
Name: "redirect",
Value: req.URL.Path,
@@ -216,7 +229,7 @@ func getOauthUser(dbConn *sql.DB, client *http.Client, uri string) (*database.Us
return nil, err
}
- user, err := database.FindOrSaveUser(dbConn, userStruct)
+ user, err := database.FindOrSaveBaseUser(dbConn, userStruct)
if err != nil {
return nil, err
}
diff --git a/api/dns/dns.go b/api/dns/dns.go
index aa2f356..6357dfc 100644
--- a/api/dns/dns.go
+++ b/api/dns/dns.go
@@ -8,39 +8,15 @@ import (
"strconv"
"strings"
- "git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters"
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/external_dns"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/types"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/utils"
)
-func userCanFuckWithDNSRecord(dbConn *sql.DB, user *database.User, record *database.DNSRecord, ownedInternalDomainFormats []string) bool {
- ownedByUser := (user.ID == record.UserID)
- if !ownedByUser {
- return false
- }
-
- if !record.Internal {
- for _, format := range ownedInternalDomainFormats {
- domain := fmt.Sprintf(format, user.Username)
-
- isInSubDomain := strings.HasSuffix(record.Name, "."+domain)
- if domain == record.Name || isInSubDomain {
- return true
- }
- }
- return false
- }
-
- owner, err := database.FindFirstDomainOwnerId(dbConn, record.Name)
- if err != nil {
- log.Println(err)
- return false
- }
+const MaxUserRecords = 100
- userIsOwnerOfDomain := owner == user.ID
- return ownedByUser && userIsOwnerOfDomain
-}
+var UserOwnedInternalFmtDomains = []string{"%s", "%s.endpoints"}
func ListDNSRecordsContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
@@ -59,8 +35,8 @@ func ListDNSRecordsContinuation(context *types.RequestContext, req *http.Request
func CreateDNSRecordContinuation(dnsAdapter external_dns.ExternalDNSAdapter, maxUserRecords int, allowedUserDomainFormats []string) func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
- formErrors := types.FormError{
- Errors: []string{},
+ formErrors := types.BannerMessages{
+ Messages: []string{},
}
internal := req.FormValue("internal") == "on" || req.FormValue("internal") == "true"
@@ -77,7 +53,7 @@ func CreateDNSRecordContinuation(dnsAdapter external_dns.ExternalDNSAdapter, max
ttlNum, err := strconv.Atoi(ttl)
if err != nil {
resp.WriteHeader(http.StatusBadRequest)
- formErrors.Errors = append(formErrors.Errors, "invalid ttl")
+ formErrors.Messages = append(formErrors.Messages, "invalid ttl")
}
dnsRecordCount, err := database.CountUserDNSRecords(context.DBConn, context.User.ID)
@@ -88,7 +64,7 @@ func CreateDNSRecordContinuation(dnsAdapter external_dns.ExternalDNSAdapter, max
}
if dnsRecordCount >= maxUserRecords {
resp.WriteHeader(http.StatusTooManyRequests)
- formErrors.Errors = append(formErrors.Errors, "max records reached")
+ formErrors.Messages = append(formErrors.Messages, "max records reached")
}
dnsRecord := &database.DNSRecord{
@@ -102,10 +78,10 @@ func CreateDNSRecordContinuation(dnsAdapter external_dns.ExternalDNSAdapter, max
if !userCanFuckWithDNSRecord(context.DBConn, context.User, dnsRecord, allowedUserDomainFormats) {
resp.WriteHeader(http.StatusUnauthorized)
- formErrors.Errors = append(formErrors.Errors, "'name' must end with "+context.User.Username+" or you must be a domain owner for internal domains")
+ formErrors.Messages = append(formErrors.Messages, "'name' must end with "+context.User.Username+" or you must be a domain owner for internal domains")
}
- if len(formErrors.Errors) == 0 {
+ if len(formErrors.Messages) == 0 {
if dnsRecord.Internal {
dnsRecord.ID = utils.RandomId()
} else {
@@ -113,24 +89,28 @@ func CreateDNSRecordContinuation(dnsAdapter external_dns.ExternalDNSAdapter, max
if err != nil {
log.Println(err)
resp.WriteHeader(http.StatusInternalServerError)
- formErrors.Errors = append(formErrors.Errors, err.Error())
+ formErrors.Messages = append(formErrors.Messages, err.Error())
}
}
}
- if len(formErrors.Errors) == 0 {
+ if len(formErrors.Messages) == 0 {
_, err := database.SaveDNSRecord(context.DBConn, dnsRecord)
if err != nil {
log.Println(err)
- formErrors.Errors = append(formErrors.Errors, "error saving record")
+ formErrors.Messages = append(formErrors.Messages, "error saving record")
}
}
- if len(formErrors.Errors) == 0 {
+ if len(formErrors.Messages) == 0 {
+ formSuccess := types.BannerMessages{
+ Messages: []string{"record added."},
+ }
+ (*context.TemplateData)["Success"] = formSuccess
return success(context, req, resp)
}
- (*context.TemplateData)["FormError"] = &formErrors
+ (*context.TemplateData)[""] = &formErrors
(*context.TemplateData)["RecordForm"] = dnsRecord
return failure(context, req, resp)
}
@@ -168,7 +148,39 @@ func DeleteDNSRecordContinuation(dnsAdapter external_dns.ExternalDNSAdapter) fun
return failure(context, req, resp)
}
+ formSuccess := types.BannerMessages{
+ Messages: []string{"record deleted."},
+ }
+ (*context.TemplateData)["Success"] = formSuccess
return success(context, req, resp)
}
}
}
+
+func userCanFuckWithDNSRecord(dbConn *sql.DB, user *database.User, record *database.DNSRecord, ownedInternalDomainFormats []string) bool {
+ ownedByUser := (user.ID == record.UserID)
+ if !ownedByUser {
+ return false
+ }
+
+ if !record.Internal {
+ for _, format := range ownedInternalDomainFormats {
+ domain := fmt.Sprintf(format, user.Username)
+
+ isInSubDomain := strings.HasSuffix(record.Name, "."+domain)
+ if domain == record.Name || isInSubDomain {
+ return true
+ }
+ }
+ return false
+ }
+
+ owner, err := database.FindFirstDomainOwnerId(dbConn, record.Name)
+ if err != nil {
+ log.Println(err)
+ return false
+ }
+
+ userIsOwnerOfDomain := owner == user.ID
+ return ownedByUser && userIsOwnerOfDomain
+}
diff --git a/api/dns/dns_test.go b/api/dns/dns_test.go
index 43dc680..30baedf 100644
--- a/api/dns/dns_test.go
+++ b/api/dns/dns_test.go
@@ -39,7 +39,7 @@ func setup() (*sql.DB, *types.RequestContext, func()) {
Mail: "test@test.com",
DisplayName: "test",
}
- database.FindOrSaveUser(testDb, user)
+ database.FindOrSaveBaseUser(testDb, user)
context := &types.RequestContext{
DBConn: testDb,
@@ -246,7 +246,7 @@ func TestThatUserMustOwnRecordToRemove(t *testing.T) {
defer testServer.Close()
nonOwnerUser := &database.User{ID: "n/a", Username: "testuser"}
- _, err := database.FindOrSaveUser(db, nonOwnerUser)
+ _, err := database.FindOrSaveBaseUser(db, nonOwnerUser)
if err != nil {
t.Error(err)
}
diff --git a/api/guestbook/guestbook.go b/api/guestbook/guestbook.go
index 60a7b4b..c0c7892 100644
--- a/api/guestbook/guestbook.go
+++ b/api/guestbook/guestbook.go
@@ -10,37 +10,13 @@ import (
"git.hatecomputers.club/hatecomputers/hatecomputers.club/utils"
)
-func validateGuestbookEntry(entry *database.GuestbookEntry) []string {
- errors := []string{}
-
- if entry.Name == "" {
- errors = append(errors, "name is required")
- }
-
- if entry.Message == "" {
- errors = append(errors, "message is required")
- }
-
- messageLength := len(entry.Message)
- if messageLength > 500 {
- errors = append(errors, "message cannot be longer than 500 characters")
- }
-
- newLines := strings.Count(entry.Message, "\n")
- if newLines > 10 {
- errors = append(errors, "message cannot contain more than 10 new lines")
- }
-
- return errors
-}
-
func SignGuestbookContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
name := req.FormValue("name")
message := req.FormValue("message")
- formErrors := types.FormError{
- Errors: []string{},
+ formErrors := types.BannerMessages{
+ Messages: []string{},
}
entry := &database.GuestbookEntry{
@@ -48,24 +24,28 @@ func SignGuestbookContinuation(context *types.RequestContext, req *http.Request,
Name: name,
Message: message,
}
- formErrors.Errors = append(formErrors.Errors, validateGuestbookEntry(entry)...)
+ formErrors.Messages = append(formErrors.Messages, validateGuestbookEntry(entry)...)
- if len(formErrors.Errors) == 0 {
+ if len(formErrors.Messages) == 0 {
_, err := database.SaveGuestbookEntry(context.DBConn, entry)
if err != nil {
log.Println(err)
- formErrors.Errors = append(formErrors.Errors, "failed to save entry")
+ formErrors.Messages = append(formErrors.Messages, "failed to save entry")
}
}
- if len(formErrors.Errors) > 0 {
- (*context.TemplateData)["FormError"] = formErrors
+ if len(formErrors.Messages) > 0 {
+ (*context.TemplateData)["Error"] = formErrors
(*context.TemplateData)["EntryForm"] = entry
resp.WriteHeader(http.StatusBadRequest)
return failure(context, req, resp)
}
+ formSuccess := types.BannerMessages{
+ Messages: []string{"entry added."},
+ }
+ (*context.TemplateData)["Success"] = formSuccess
return success(context, req, resp)
}
}
@@ -83,3 +63,27 @@ func ListGuestbookContinuation(context *types.RequestContext, req *http.Request,
return success(context, req, resp)
}
}
+
+func validateGuestbookEntry(entry *database.GuestbookEntry) []string {
+ errors := []string{}
+
+ if entry.Name == "" {
+ errors = append(errors, "name is required")
+ }
+
+ if entry.Message == "" {
+ errors = append(errors, "message is required")
+ }
+
+ messageLength := len(entry.Message)
+ if messageLength > 500 {
+ errors = append(errors, "message cannot be longer than 500 characters")
+ }
+
+ newLines := strings.Count(entry.Message, "\n")
+ if newLines > 10 {
+ errors = append(errors, "message cannot contain more than 10 new lines")
+ }
+
+ return errors
+}
diff --git a/api/hcaptcha/hcaptcha.go b/api/hcaptcha/hcaptcha.go
index 007190d..e8ea238 100644
--- a/api/hcaptcha/hcaptcha.go
+++ b/api/hcaptcha/hcaptcha.go
@@ -62,8 +62,8 @@ func CaptchaVerificationContinuation(context *types.RequestContext, req *http.Re
err := verifyCaptcha(secretKey, hCaptchaResponse)
if err != nil {
- (*context.TemplateData)["FormError"] = types.FormError{
- Errors: []string{"hCaptcha verification failed"},
+ (*context.TemplateData)["Error"] = types.BannerMessages{
+ Messages: []string{"hCaptcha verification failed"},
}
resp.WriteHeader(http.StatusBadRequest)
diff --git a/api/keys/keys.go b/api/keys/keys.go
index cef3f3c..7702f3d 100644
--- a/api/keys/keys.go
+++ b/api/keys/keys.go
@@ -27,8 +27,8 @@ func ListAPIKeysContinuation(context *types.RequestContext, req *http.Request, r
func CreateAPIKeyContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
- formErrors := types.FormError{
- Errors: []string{},
+ formErrors := types.BannerMessages{
+ Messages: []string{},
}
numKeys, err := database.CountUserAPIKeys(context.DBConn, context.User.ID)
@@ -39,11 +39,11 @@ func CreateAPIKeyContinuation(context *types.RequestContext, req *http.Request,
}
if numKeys >= MAX_USER_API_KEYS {
- formErrors.Errors = append(formErrors.Errors, "max types keys reached")
+ formErrors.Messages = append(formErrors.Messages, "max types keys reached")
}
- if len(formErrors.Errors) > 0 {
- (*context.TemplateData)["FormError"] = formErrors
+ if len(formErrors.Messages) > 0 {
+ (*context.TemplateData)["Error"] = formErrors
return failure(context, req, resp)
}
@@ -56,6 +56,11 @@ func CreateAPIKeyContinuation(context *types.RequestContext, req *http.Request,
resp.WriteHeader(http.StatusInternalServerError)
return failure(context, req, resp)
}
+
+ formSuccess := types.BannerMessages{
+ Messages: []string{"key created."},
+ }
+ (*context.TemplateData)["Success"] = formSuccess
return success(context, req, resp)
}
}
@@ -82,6 +87,11 @@ func DeleteAPIKeyContinuation(context *types.RequestContext, req *http.Request,
return failure(context, req, resp)
}
+ formSuccess := types.BannerMessages{
+ Messages: []string{"key deleted."},
+ }
+ (*context.TemplateData)["Success"] = formSuccess
+
return success(context, req, resp)
}
}
diff --git a/api/profiles/profiles.go b/api/profiles/profiles.go
new file mode 100644
index 0000000..8e10e5f
--- /dev/null
+++ b/api/profiles/profiles.go
@@ -0,0 +1,118 @@
+package profiles
+
+import (
+ "log"
+ "net/http"
+ "strings"
+
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/files"
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/api/types"
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
+)
+
+const MaxAvatarSize = 1024 * 1024 * 2 // 2MB
+const AvatarPath = "avatars/"
+const AvatarPrefix = "/uploads/avatars/"
+
+func GetProfileContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
+ if context.User == nil {
+ return failure(context, req, resp)
+ }
+
+ (*context.TemplateData)["Profile"] = context.User
+ return success(context, req, resp)
+ }
+}
+
+func UpdateProfileContinuation(fileAdapter files.FilesAdapter, maxAvatarSize int, avatarPath string, avatarPrefix string) func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
+ formErrors := types.BannerMessages{
+ Messages: []string{},
+ }
+
+ err := req.ParseMultipartForm(int64(maxAvatarSize))
+ if err != nil {
+ formErrors.Messages = append(formErrors.Messages, "avatar file too large")
+ }
+
+ if len(formErrors.Messages) == 0 {
+ file, _, err := req.FormFile("avatar")
+ if file != nil && err != nil {
+ formErrors.Messages = append(formErrors.Messages, "error uploading avatar")
+ } else if file != nil {
+ defer file.Close()
+ reader := http.MaxBytesReader(resp, file, int64(maxAvatarSize))
+ defer reader.Close()
+
+ _, err = fileAdapter.CreateFile(avatarPath+context.User.ID, reader)
+ if err != nil {
+ log.Println(err)
+ formErrors.Messages = append(formErrors.Messages, "error saving avatar (is it too big?)")
+ }
+ }
+ }
+
+ context.User.Bio = strings.Trim(req.FormValue("bio"), "\n")
+ context.User.Pronouns = req.FormValue("pronouns")
+ context.User.Location = req.FormValue("location")
+ context.User.Website = req.FormValue("website")
+ context.User.Avatar = avatarPrefix + context.User.ID
+ formErrors.Messages = append(formErrors.Messages, validateProfileUpdate(context.User)...)
+
+ if len(formErrors.Messages) == 0 {
+ _, err = database.SaveUser(context.DBConn, context.User)
+ if err != nil {
+ formErrors.Messages = append(formErrors.Messages, "error saving profile")
+ }
+ }
+
+ (*context.TemplateData)["Profile"] = context.User
+ (*context.TemplateData)["Error"] = formErrors
+
+ if len(formErrors.Messages) > 0 {
+ log.Println(formErrors.Messages)
+
+ resp.WriteHeader(http.StatusBadRequest)
+ return failure(context, req, resp)
+ }
+
+ formSuccess := types.BannerMessages{
+ Messages: []string{"profile updated"},
+ }
+ (*context.TemplateData)["Success"] = formSuccess
+ return success(context, req, resp)
+ }
+ }
+}
+
+func validateProfileUpdate(user *database.User) []string {
+ errors := []string{}
+
+ if (!strings.HasPrefix(user.Website, "https://") && !strings.HasPrefix(user.Website, "http://")) || len(user.Website) < 8 {
+ errors = append(errors, "website must be a valid URL")
+ }
+ if len(user.Website) > 64 {
+ errors = append(errors, "website cannot be longer than 64 characters")
+ }
+
+ if len(user.Pronouns) > 64 {
+ errors = append(errors, "pronouns cannot be longer than 64 characters")
+ }
+
+ if len(user.Bio) > 128 {
+ errors = append(errors, "bio cannot be longer than 128 characters")
+ }
+
+ newLines := strings.Count(user.Bio, "\n")
+ if newLines > 8 {
+ errors = append(errors, "message cannot contain more than 8 new lines")
+ }
+
+ if len(user.Location) > 32 {
+ errors = append(errors, "location cannot be longer than 64 characters")
+ }
+
+ return errors
+}
diff --git a/api/serve.go b/api/serve.go
index c8775d8..a688445 100644
--- a/api/serve.go
+++ b/api/serve.go
@@ -7,12 +7,14 @@ import (
"net/http"
"time"
- "git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/cloudflare"
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/external_dns/cloudflare"
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/files/filesystem"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/auth"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/dns"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/guestbook"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/hcaptcha"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/keys"
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/api/profiles"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/template"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/types"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/args"
@@ -32,7 +34,6 @@ func LogRequestContinuation(context *types.RequestContext, req *http.Request, re
func LogExecutionTimeContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(success types.Continuation, _failure types.Continuation) types.ContinuationChain {
end := time.Now()
-
log.Println(context.Id, "took", end.Sub(context.Start))
return success(context, req, resp)
@@ -70,13 +71,15 @@ func CacheControlMiddleware(next http.Handler, maxAge int) http.Handler {
func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
mux := http.NewServeMux()
- fileServer := http.FileServer(http.Dir(argv.StaticPath))
- mux.Handle("GET /static/", http.StripPrefix("/static/", CacheControlMiddleware(fileServer, 3600)))
-
+ // "dependency injection"
cloudflareAdapter := &cloudflare.CloudflareExternalDNSAdapter{
APIToken: argv.CloudflareToken,
ZoneId: argv.CloudflareZone,
}
+ uploadAdapter := &filesystem.FilesystemAdapter{
+ BasePath: argv.UploadPath,
+ Permissions: 0777,
+ }
makeRequestContext := func() *types.RequestContext {
return &types.RequestContext{
@@ -86,9 +89,14 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
}
}
+ staticFileServer := http.FileServer(http.Dir(argv.StaticPath))
+ uploadFileServer := http.FileServer(http.Dir(argv.UploadPath))
+ mux.Handle("GET /static/", http.StripPrefix("/static/", CacheControlMiddleware(staticFileServer, 3600)))
+ mux.Handle("GET /uploads/", http.StripPrefix("/uploads/", CacheControlMiddleware(uploadFileServer, 60)))
+
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
requestContext := makeRequestContext()
- LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(template.TemplateContinuation("home.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(auth.ListUsersContinuation, auth.ListUsersContinuation)(template.TemplateContinuation("home.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
})
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
@@ -111,16 +119,26 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
LogRequestContinuation(requestContext, r, w)(auth.LogoutContinuation, FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
})
+ mux.HandleFunc("GET /profile", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(profiles.GetProfileContinuation, auth.GoLoginContinuation)(template.TemplateContinuation("profile.html", true), template.TemplateContinuation("profile.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
+ mux.HandleFunc("POST /profile", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ updateProfileContinuation := profiles.UpdateProfileContinuation(uploadAdapter, profiles.MaxAvatarSize, profiles.AvatarPath, profiles.AvatarPrefix)
+
+ LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(updateProfileContinuation, auth.GoLoginContinuation)(profiles.GetProfileContinuation, FailurePassingContinuation)(template.TemplateContinuation("profile.html", true), template.TemplateContinuation("profile.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
mux.HandleFunc("GET /dns", func(w http.ResponseWriter, r *http.Request) {
requestContext := makeRequestContext()
LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(dns.ListDNSRecordsContinuation, auth.GoLoginContinuation)(template.TemplateContinuation("dns.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
})
- const MAX_USER_RECORDS = 100
- var USER_OWNED_INTERNAL_FMT_DOMAINS = []string{"%s", "%s.endpoints"}
mux.HandleFunc("POST /dns", func(w http.ResponseWriter, r *http.Request) {
requestContext := makeRequestContext()
- LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(dns.ListDNSRecordsContinuation, auth.GoLoginContinuation)(dns.CreateDNSRecordContinuation(cloudflareAdapter, MAX_USER_RECORDS, USER_OWNED_INTERNAL_FMT_DOMAINS), FailurePassingContinuation)(dns.ListDNSRecordsContinuation, dns.ListDNSRecordsContinuation)(template.TemplateContinuation("dns.html", true), template.TemplateContinuation("dns.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(dns.ListDNSRecordsContinuation, auth.GoLoginContinuation)(dns.CreateDNSRecordContinuation(cloudflareAdapter, dns.MaxUserRecords, dns.UserOwnedInternalFmtDomains), FailurePassingContinuation)(dns.ListDNSRecordsContinuation, dns.ListDNSRecordsContinuation)(template.TemplateContinuation("dns.html", true), template.TemplateContinuation("dns.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
})
mux.HandleFunc("POST /dns/delete", func(w http.ResponseWriter, r *http.Request) {
diff --git a/api/types/types.go b/api/types/types.go
index bbc25ea..84ed93c 100644
--- a/api/types/types.go
+++ b/api/types/types.go
@@ -20,8 +20,8 @@ type RequestContext struct {
User *database.User
}
-type FormError struct {
- Errors []string
+type BannerMessages struct {
+ Messages []string
}
type Continuation func(*RequestContext, *http.Request, http.ResponseWriter) ContinuationChain
diff --git a/args/args.go b/args/args.go
index 09c96be..59eb441 100644
--- a/args/args.go
+++ b/args/args.go
@@ -13,6 +13,7 @@ type Arguments struct {
DatabasePath string
TemplatePath string
StaticPath string
+ UploadPath string
Migrate bool
Scheduler bool
@@ -35,6 +36,13 @@ type Arguments struct {
func GetArgs() (*Arguments, error) {
databasePath := flag.String("database-path", "./hatecomputers.db", "Path to the SQLite database")
+
+ uploadPath := flag.String("upload-path", "./uploads", "Path to the uploads directory")
+ uploadPathValue := *uploadPath
+ if uploadPathValue[len(uploadPathValue)-1] != '/' {
+ uploadPathValue += "/"
+ }
+
templatePath := flag.String("template-path", "./templates", "Path to the template directory")
staticPath := flag.String("static-path", "./static", "Path to the static directory")
dnsResolvers := flag.String("dns-resolvers", "1.1.1.1:53,1.0.0.1:53", "Comma-separated list of DNS resolvers")
@@ -96,6 +104,7 @@ func GetArgs() (*Arguments, error) {
arguments := &Arguments{
DatabasePath: *databasePath,
TemplatePath: *templatePath,
+ UploadPath: uploadPathValue,
StaticPath: *staticPath,
CloudflareToken: cloudflareToken,
CloudflareZone: cloudflareZone,
diff --git a/database/migrate.go b/database/migrate.go
index a117480..e9e21b7 100644
--- a/database/migrate.go
+++ b/database/migrate.go
@@ -4,6 +4,7 @@ import (
"log"
"database/sql"
+
_ "github.com/mattn/go-sqlite3"
)
@@ -127,6 +128,40 @@ func MigrateGuestBook(dbConn *sql.DB) (*sql.DB, error) {
return dbConn, nil
}
+func MigrateProfiles(dbConn *sql.DB) (*sql.DB, error) {
+ log.Println("migrating profiles columns")
+
+ userColumns := map[string]bool{}
+ row, err := dbConn.Query(`PRAGMA table_info(users);`)
+ if err != nil {
+ return dbConn, err
+ }
+ defer row.Close()
+
+ for row.Next() {
+ var columnName string
+ row.Scan(nil, &columnName, nil, nil, nil, nil)
+ userColumns[columnName] = true
+ }
+
+ columns := map[string]string{}
+ columns["pronouns"] = "unspecified"
+ columns["bio"] = "a computer hater"
+ columns["location"] = "earth"
+ columns["website"] = "https://hatecomputers.club"
+ columns["avatar"] = "/static/img/default-avatar.png"
+
+ for column, defaultValue := range columns {
+ if userColumns[column] {
+ continue
+ }
+ log.Println("migrating column", column)
+ _, err = dbConn.Exec(`ALTER TABLE users ADD COLUMN ` + column + ` TEXT NOT NULL DEFAULT '` + defaultValue + `';`)
+ }
+
+ return dbConn, nil
+}
+
func Migrate(dbConn *sql.DB) (*sql.DB, error) {
log.Println("migrating database")
@@ -137,6 +172,7 @@ func Migrate(dbConn *sql.DB) (*sql.DB, error) {
MigrateDomainOwners,
MigrateDNSRecords,
MigrateGuestBook,
+ MigrateProfiles,
}
for _, migration := range migrations {
diff --git a/database/users.go b/database/users.go
index 6f9456e..804b723 100644
--- a/database/users.go
+++ b/database/users.go
@@ -24,6 +24,11 @@ type User struct {
Mail string `json:"email"`
Username string `json:"preferred_username"`
DisplayName string `json:"name"`
+ Bio string `json:"bio"`
+ Location string `json:"location"`
+ Website string `json:"website"`
+ Pronouns string `json:"pronouns"` // liberals!! :O
+ Avatar string `json:"avatar"`
CreatedAt time.Time `json:"created_at"`
}
@@ -33,13 +38,38 @@ type UserSession struct {
ExpireAt time.Time `json:"expire_at"`
}
+func ListUsers(dbConn *sql.DB) ([]*User, error) {
+ log.Println("listing users")
+
+ rows, err := dbConn.Query(`SELECT id, mail, username, display_name, bio, location, website, avatar, pronouns, created_at FROM users;`)
+ if err != nil {
+ log.Println(err)
+ return nil, err
+ }
+ defer rows.Close()
+
+ var users []*User
+ for rows.Next() {
+ var user User
+ err := rows.Scan(&user.ID, &user.Mail, &user.Username, &user.DisplayName, &user.Bio, &user.Location, &user.Website, &user.Avatar, &user.Pronouns, &user.CreatedAt)
+ if err != nil {
+ log.Println(err)
+ return nil, err
+ }
+
+ users = append(users, &user)
+ }
+
+ return users, nil
+}
+
func GetUser(dbConn *sql.DB, id string) (*User, error) {
log.Println("getting user", id)
- row := dbConn.QueryRow(`SELECT id, mail, username, display_name, created_at FROM users WHERE id = ?;`, id)
+ row := dbConn.QueryRow(`SELECT id, mail, username, display_name, bio, location, website, avatar, pronouns, created_at FROM users WHERE id = ?;`, id)
var user User
- err := row.Scan(&user.ID, &user.Mail, &user.Username, &user.DisplayName, &user.CreatedAt)
+ err := row.Scan(&user.ID, &user.Mail, &user.Username, &user.DisplayName, &user.Bio, &user.Location, &user.Website, &user.Avatar, &user.Pronouns, &user.CreatedAt)
if err != nil {
log.Println(err)
return nil, err
@@ -48,7 +78,7 @@ func GetUser(dbConn *sql.DB, id string) (*User, error) {
return &user, nil
}
-func FindOrSaveUser(dbConn *sql.DB, user *User) (*User, error) {
+func FindOrSaveBaseUser(dbConn *sql.DB, user *User) (*User, error) {
log.Println("finding or saving user", user.ID)
_, err := dbConn.Exec(`INSERT INTO users (id, mail, username, display_name) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET mail = excluded.mail, username = excluded.username, display_name = excluded.display_name;`, user.ID, user.Mail, user.Username, user.DisplayName)
@@ -59,6 +89,17 @@ func FindOrSaveUser(dbConn *sql.DB, user *User) (*User, error) {
return user, nil
}
+func SaveUser(dbConn *sql.DB, user *User) (*User, error) {
+ log.Println("saving user", user.ID)
+
+ _, err := dbConn.Exec(`INSERT INTO users (id, mail, username, display_name, bio, location, website, pronouns, avatar) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET mail = excluded.mail, username = excluded.username, display_name = excluded.display_name, bio = excluded.bio, location = excluded.location, website = excluded.website, pronouns = excluded.pronouns, avatar = excluded.avatar;`, user.ID, user.Mail, user.Username, user.DisplayName, user.Bio, user.Location, user.Website, user.Pronouns, user.Avatar)
+ if err != nil {
+ return nil, err
+ }
+
+ return user, nil
+}
+
func MakeUserSessionFor(dbConn *sql.DB, user *User) (*UserSession, error) {
log.Println("making session for user", user.ID)
diff --git a/docker-compose.yml b/docker-compose.yml
index b568e87..957683f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,5 +15,6 @@ services:
- ./db:/app/db
- ./templates:/app/templates
- ./static:/app/static
+ - ./uploads:/app/uploads
ports:
- "127.0.0.1:4455:8080"
diff --git a/hcdns/server_test.go b/hcdns/server_test.go
index f1b283f..4fdf03d 100644
--- a/hcdns/server_test.go
+++ b/hcdns/server_test.go
@@ -27,7 +27,7 @@ func setup(arguments *args.Arguments) (*sql.DB, *dns.Server, string, func()) {
testUser := &database.User{
ID: "test",
}
- database.FindOrSaveUser(testDb, testUser)
+ database.FindOrSaveBaseUser(testDb, testUser)
dnsArguments := arguments
if dnsArguments == nil {
diff --git a/static/css/club.css b/static/css/club.css
new file mode 100644
index 0000000..747f2d0
--- /dev/null
+++ b/static/css/club.css
@@ -0,0 +1,48 @@
+.club-members {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: left;
+ gap: 20px;
+ padding: 20px;
+}
+
+.club-member {
+ flex: 1;
+ background-color: var(--background-color-2);
+ border: 1px solid var(--border-color);
+ padding: 10px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-around;
+ gap: 10px;
+ max-width: 600px;
+ min-width: 400px;
+ line-break: anywhere;
+}
+
+.club-bio {
+ white-space: pre-wrap;
+ border-top: 1px solid var(--border-color);
+}
+
+.avatar {
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.avatar div {
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-size: cover;
+ width: 120px;
+ height: 120px;
+ border-radius: 25%;
+}
+
+.about {
+ flex: 2;
+}
diff --git a/static/css/colors.css b/static/css/colors.css
index c68bf8e..46357d9 100644
--- a/static/css/colors.css
+++ b/static/css/colors.css
@@ -1,7 +1,8 @@
:root {
--background-color-light: #f4e8e9;
- --background-color-light-2: #f7f7f7;
+ --background-color-light-2: #f5e6f3;
--text-color-light: #333;
+ --confirm-color-light: #91d9bb;
--link-color-light: #d291bc;
--container-bg-light: #fff7f87a;
--border-color-light: #692fcc;
@@ -10,6 +11,7 @@
--background-color-dark: #333;
--background-color-dark-2: #2c2c2c;
--text-color-dark: #f4e8e9;
+ --confirm-color-dark: #4d8f73;
--link-color-dark: #b86b77;
--container-bg-dark: #424242ea;
--border-color-dark: #956ade;
@@ -24,6 +26,7 @@
--container-bg: var(--container-bg-dark);
--border-color: var(--border-color-dark);
--error-color: var(--error-color-dark);
+ --confirm-color: var(--confirm-color-dark);
}
[data-theme="LIGHT"] {
@@ -34,9 +37,15 @@
--container-bg: var(--container-bg-light);
--border-color: var(--border-color-light);
--error-color: var(--error-color-light);
+ --confirm-color: var(--confirm-color-light);
}
.error {
background-color: var(--error-color);
padding: 1rem;
}
+
+.success {
+ background-color: var(--confirm-color);
+ padding: 1rem;
+}
diff --git a/static/css/form.css b/static/css/form.css
index a5dc358..7ccd8db 100644
--- a/static/css/form.css
+++ b/static/css/form.css
@@ -36,4 +36,7 @@ textarea {
margin: 0 0 1em;
border: 1px solid var(--border-color);
background: var(--container-bg);
+
+ resize: vertical;
+ min-height: 100px;
}
diff --git a/static/css/guestbook.css b/static/css/guestbook.css
index 0fb7a16..6241717 100644
--- a/static/css/guestbook.css
+++ b/static/css/guestbook.css
@@ -3,6 +3,7 @@
border: 1px solid var(--border-color);
padding: 10px;
+ max-width: 700px;
}
.entry-name {
diff --git a/static/css/styles.css b/static/css/styles.css
index ba58018..886052e 100644
--- a/static/css/styles.css
+++ b/static/css/styles.css
@@ -3,6 +3,7 @@
@import "/static/css/table.css";
@import "/static/css/form.css";
@import "/static/css/guestbook.css";
+@import "/static/css/club.css";
@font-face {
font-family: "ComicSans";
@@ -22,15 +23,27 @@
}
@-webkit-keyframes cursor {
- 0% {cursor: url("/static/img/cursor-2.png"), auto;}
- 50% {cursor: url("/static/img/cursor-1.png"), auto;}
- 100% {cursor: url("/static/img/cursor-2.png"), auto;}
+ 0% {
+ cursor: url("/static/img/cursor-2.png"), auto;
+ }
+ 50% {
+ cursor: url("/static/img/cursor-1.png"), auto;
+ }
+ 100% {
+ cursor: url("/static/img/cursor-2.png"), auto;
+ }
}
@keyframes cursor {
- 0% {cursor: url("/static/img/cursor-2.png"), auto;}
- 50% {cursor: url("/static/img/cursor-1.png"), auto;}
- 100% {cursor: url("/static/img/cursor-2.png"), auto;}
+ 0% {
+ cursor: url("/static/img/cursor-2.png"), auto;
+ }
+ 50% {
+ cursor: url("/static/img/cursor-1.png"), auto;
+ }
+ 100% {
+ cursor: url("/static/img/cursor-2.png"), auto;
+ }
}
body {
@@ -70,3 +83,14 @@ hr {
max-width: 900px;
gap: 10px 10px;
}
+
+.info {
+ margin-bottom: 1rem;
+ max-width: 600px;
+
+ transition: opacity 0.3s;
+}
+
+.info:hover {
+ opacity: 0.8;
+}
diff --git a/static/img/default-avatar.png b/static/img/default-avatar.png
new file mode 100644
index 0000000..66a38c2
--- /dev/null
+++ b/static/img/default-avatar.png
Binary files differ
diff --git a/static/js/components/infoBanners.js b/static/js/components/infoBanners.js
new file mode 100644
index 0000000..6a19864
--- /dev/null
+++ b/static/js/components/infoBanners.js
@@ -0,0 +1,6 @@
+const infoBanners = document.querySelectorAll(".info");
+Array.from(infoBanners).forEach((infoBanner) => {
+ infoBanner.addEventListener("click", () => {
+ infoBanner.remove();
+ });
+});
diff --git a/static/js/script.js b/static/js/script.js
index 56233e3..b5e6249 100644
--- a/static/js/script.js
+++ b/static/js/script.js
@@ -1,5 +1,6 @@
const scripts = [
"/static/js/components/themeSwitcher.js",
"/static/js/components/formatDate.js",
+ "/static/js/components/infoBanners.js",
];
requirejs(scripts);
diff --git a/templates/api_keys.html b/templates/api_keys.html
index cd4d274..018fda3 100644
--- a/templates/api_keys.html
+++ b/templates/api_keys.html
@@ -28,12 +28,5 @@
<h2>generate key.</h2>
<hr>
<input type="submit" value="generate." />
- {{ if .FormError }}
- {{ if (len .FormError.Errors) }}
- {{ range $error := .FormError.Errors }}
- <div class="error">{{ $error }}</div>
- {{ end }}
- {{ end }}
- {{ end }}
</form>
{{ end }}
diff --git a/templates/base.html b/templates/base.html
index 9f5a903..89d6dd2 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -37,7 +37,9 @@
<span> | </span>
<a href="/keys">api keys.</a>
<span> | </span>
- <a href="/logout">logout, {{ .User.DisplayName }}.</a>
+ <a href="/profile">{{ .User.DisplayName }}.</a>
+ <span> | </span>
+ <a href="/logout">logout.</a>
{{ else }}
<a href="/login">login.</a>
@@ -46,6 +48,17 @@
<hr>
<div id="content">
+ {{ if and .Success (gt (len .Success.Messages) 0) }}
+ {{ range $message := .Success.Messages }}
+ <div class="info success">{{ $message }}</div>
+ {{ end }}
+ {{ end }}
+ {{ if and .Error (gt (len .Error.Messages) 0) }}
+ {{ range $error := .Error.Messages }}
+ <div class="info error">{{ $error }}</div>
+ {{ end }}
+ {{ end }}
+
{{ template "content" . }}
</div>
diff --git a/templates/dns.html b/templates/dns.html
index d16ed89..2f3f0a7 100644
--- a/templates/dns.html
+++ b/templates/dns.html
@@ -76,18 +76,6 @@
{{ end }}
/>
</label>
-
-
- <input type="submit" value="Add" />
-
- {{ if .FormError }}
- {{ if (len .FormError.Errors) }}
- {{ range $error := .FormError.Errors }}
- <div class="error">{{ $error }}</div>
- {{ end }}
- {{ end }}
- {{ end }}
+ <input type="submit" value="add." />
</form>
-
-
{{ end }}
diff --git a/templates/guestbook.html b/templates/guestbook.html
index 85727c7..d1f4417 100644
--- a/templates/guestbook.html
+++ b/templates/guestbook.html
@@ -21,18 +21,9 @@
<div
class="h-captcha"
data-sitekey="{{ .HcaptchaArgs.SiteKey }}"
- data-theme="dark"
></div>
<br>
<button type="submit" class="btn btn-primary">sign.</button>
- <br>
- {{ if .FormError }}
- {{ if (len .FormError.Errors) }}
- {{ range $error := .FormError.Errors }}
- <div class="error">{{ $error }}</div>
- {{ end }}
- {{ end }}
- {{ end }}
</form>
<hr>
diff --git a/templates/home.html b/templates/home.html
index 1c03377..76bbc6a 100644
--- a/templates/home.html
+++ b/templates/home.html
@@ -1,3 +1,19 @@
{{ define "content" }}
- <p class="blinky">under construction!</p>
+<h2 class="blinky">hello there!</h2>
+<p>current peeps in the club :D</p>
+<div class="club-members">
+ {{ range $user := .Users }}
+ <div class="club-member">
+ <div class="avatar">
+ <div style="background-image: url('{{ $user.Avatar }}')"></div>
+ </div>
+ <div class="about">
+ <div>name: {{ $user.Username }}</div>
+ <div>pronouns: {{ $user.Pronouns }}</div>
+ <div><a href="{{ $user.Website }}">{{ $user.Website }}</a></div>
+ <div class="club-bio">{{ $user.Bio }}</div>
+ </div>
+ </div>
+ {{ end }}
+</div>
{{ end }}
diff --git a/templates/profile.html b/templates/profile.html
new file mode 100644
index 0000000..a6e1b68
--- /dev/null
+++ b/templates/profile.html
@@ -0,0 +1,24 @@
+{{ define "content" }}
+
+<h1>hey {{ .Profile.DisplayName }}</h1>
+<br>
+<form action="/profile" method="POST" class="form" enctype="multipart/form-data">
+ <label for="file" class="file-upload">avatar.</label>
+ <input type="file" name="avatar">
+
+ <label for="location">location.</label>
+ <input type="text" name="location" value="{{ .Profile.Location }}">
+
+ <label for="website">website.</label>
+ <input type="text" name="website" value="{{ .Profile.Website }}">
+
+ <label for="pronouns">pronouns.</label>
+ <input type="text" name="pronouns" value="{{ .Profile.Pronouns }}">
+
+ <label for="bio">bio.</label>
+ <textarea name="bio">{{ .Profile.Bio }}</textarea>
+
+ <input type="submit" value="update">
+</form>
+
+{{ end }}
diff --git a/utils/RandomId.go b/utils/random_id.go
index 1b03ec8..1b03ec8 100644
--- a/utils/RandomId.go
+++ b/utils/random_id.go