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/api/auth/auth.go b/api/auth/auth.go index c54aad6..04d6c12 100644 --- a/api/auth/auth.go +++ b/api/auth/auth.go @@ -170,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, diff --git a/api/dns/dns.go b/api/dns/dns.go index bf91994..6357dfc 100644 --- a/api/dns/dns.go +++ b/api/dns/dns.go @@ -18,34 +18,6 @@ const MaxUserRecords = 100 var UserOwnedInternalFmtDomains = []string{"%s", "%s.endpoints"} -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 -} - func ListDNSRecordsContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID) @@ -63,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" @@ -81,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) @@ -92,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{ @@ -106,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 { @@ -117,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) } @@ -172,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/guestbook/guestbook.go b/api/guestbook/guestbook.go index 60a7b4b..c0c7892 100644 --- a/api/guestbook/guestbook.go +++ b/api/guestbook/guestbook.go @@ -10,6 +10,60 @@ import ( "git.hatecomputers.club/hatecomputers/hatecomputers.club/utils" ) +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.BannerMessages{ + Messages: []string{}, + } + + entry := &database.GuestbookEntry{ + ID: utils.RandomId(), + Name: name, + Message: message, + } + formErrors.Messages = append(formErrors.Messages, validateGuestbookEntry(entry)...) + + if len(formErrors.Messages) == 0 { + _, err := database.SaveGuestbookEntry(context.DBConn, entry) + if err != nil { + log.Println(err) + formErrors.Messages = append(formErrors.Messages, "failed to save entry") + } + } + + 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) + } +} + +func ListGuestbookContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { + entries, err := database.GetGuestbookEntries(context.DBConn) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + + (*context.TemplateData)["GuestbookEntries"] = entries + return success(context, req, resp) + } +} + func validateGuestbookEntry(entry *database.GuestbookEntry) []string { errors := []string{} @@ -33,53 +87,3 @@ func validateGuestbookEntry(entry *database.GuestbookEntry) []string { 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{}, - } - - entry := &database.GuestbookEntry{ - ID: utils.RandomId(), - Name: name, - Message: message, - } - formErrors.Errors = append(formErrors.Errors, validateGuestbookEntry(entry)...) - - if len(formErrors.Errors) == 0 { - _, err := database.SaveGuestbookEntry(context.DBConn, entry) - if err != nil { - log.Println(err) - formErrors.Errors = append(formErrors.Errors, "failed to save entry") - } - } - - if len(formErrors.Errors) > 0 { - (*context.TemplateData)["FormError"] = formErrors - (*context.TemplateData)["EntryForm"] = entry - resp.WriteHeader(http.StatusBadRequest) - - return failure(context, req, resp) - } - - return success(context, req, resp) - } -} - -func ListGuestbookContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { - return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { - entries, err := database.GetGuestbookEntries(context.DBConn) - if err != nil { - log.Println(err) - resp.WriteHeader(http.StatusInternalServerError) - return failure(context, req, resp) - } - - (*context.TemplateData)["GuestbookEntries"] = entries - return success(context, req, resp) - } -} 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 index c46d2a2..8e10e5f 100644 --- a/api/profiles/profiles.go +++ b/api/profiles/profiles.go @@ -3,6 +3,7 @@ package profiles import ( "log" "net/http" + "strings" "git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/files" "git.hatecomputers.club/hatecomputers/hatecomputers.club/api/types" @@ -27,71 +28,91 @@ func GetProfileContinuation(context *types.RequestContext, req *http.Request, re 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.FormError{ - Errors: []string{}, + formErrors := types.BannerMessages{ + Messages: []string{}, } err := req.ParseMultipartForm(int64(maxAvatarSize)) if err != nil { - log.Println(err) - - formErrors.Errors = append(formErrors.Errors, "avatar file too large") + formErrors.Messages = append(formErrors.Messages, "avatar file too large") } - if len(formErrors.Errors) == 0 { + if len(formErrors.Messages) == 0 { file, _, err := req.FormFile("avatar") - if file == nil { - formErrors.Errors = append(formErrors.Errors, "avatar required") - } - if err != nil { - formErrors.Errors = append(formErrors.Errors, "error uploading avatar") - } else { + 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, file) + _, err = fileAdapter.CreateFile(avatarPath+context.User.ID, reader) if err != nil { log.Println(err) - formErrors.Errors = append(formErrors.Errors, "error saving avatar") + formErrors.Messages = append(formErrors.Messages, "error saving avatar (is it too big?)") } } } - bio := req.FormValue("bio") - location := req.FormValue("location") - website := req.FormValue("website") - if len(bio) > 128 { - formErrors.Errors = append(formErrors.Errors, "bio too long, keep it to 128") - } - if len(location) > 32 { - formErrors.Errors = append(formErrors.Errors, "location too long, keep it to 32") - } - if len(website) > 64 { - formErrors.Errors = append(formErrors.Errors, "website too long, keep it to 64") - } - - if len(formErrors.Errors) == 0 { - context.User.Bio = bio - context.User.Location = location - context.User.Website = website - context.User.Avatar = avatarPrefix + context.User.ID + 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.Errors = append(formErrors.Errors, "error saving profile") + formErrors.Messages = append(formErrors.Messages, "error saving profile") } } (*context.TemplateData)["Profile"] = context.User - (*context.TemplateData)["FormError"] = formErrors + (*context.TemplateData)["Error"] = formErrors - if len(formErrors.Errors) > 0 { - log.Println(formErrors.Errors) + 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 c129098..a688445 100644 --- a/api/serve.go +++ b/api/serve.go @@ -92,7 +92,7 @@ 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, 3600))) + mux.Handle("GET /uploads/", http.StripPrefix("/uploads/", CacheControlMiddleware(uploadFileServer, 60))) mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { requestContext := makeRequestContext() @@ -126,7 +126,9 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server { mux.HandleFunc("POST /profile", func(w http.ResponseWriter, r *http.Request) { requestContext := makeRequestContext() - LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(profiles.UpdateProfileContinuation(uploadAdapter, profiles.MaxAvatarSize, profiles.AvatarPath, profiles.AvatarPrefix), auth.GoLoginContinuation)(profiles.GetProfileContinuation, FailurePassingContinuation)(template.TemplateContinuation("profile.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + 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) { 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/database/migrate.go b/database/migrate.go index fbba6ff..e9e21b7 100644 --- a/database/migrate.go +++ b/database/migrate.go @@ -4,6 +4,7 @@ import ( "log" "database/sql" + _ "github.com/mattn/go-sqlite3" ) @@ -144,6 +145,7 @@ func MigrateProfiles(dbConn *sql.DB) (*sql.DB, error) { } columns := map[string]string{} + columns["pronouns"] = "unspecified" columns["bio"] = "a computer hater" columns["location"] = "earth" columns["website"] = "https://hatecomputers.club" diff --git a/database/users.go b/database/users.go index cb1b3d2..804b723 100644 --- a/database/users.go +++ b/database/users.go @@ -27,6 +27,7 @@ type User struct { 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"` } @@ -37,25 +38,10 @@ type UserSession struct { ExpireAt time.Time `json:"expire_at"` } -func GetUser(dbConn *sql.DB, id string) (*User, error) { - log.Println("getting user", id) - - row := dbConn.QueryRow(`SELECT id, mail, username, display_name, bio, location, website, avatar, created_at FROM users WHERE id = ?;`, id) - - var user User - err := row.Scan(&user.ID, &user.Mail, &user.Username, &user.DisplayName, &user.Bio, &user.Location, &user.Website, &user.Avatar, &user.CreatedAt) - if err != nil { - log.Println(err) - return nil, err - } - - return &user, nil -} - 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, created_at FROM 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 @@ -65,7 +51,7 @@ func ListUsers(dbConn *sql.DB) ([]*User, error) { 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.CreatedAt) + 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 @@ -77,6 +63,21 @@ func ListUsers(dbConn *sql.DB) ([]*User, error) { 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, 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.Bio, &user.Location, &user.Website, &user.Avatar, &user.Pronouns, &user.CreatedAt) + if err != nil { + log.Println(err) + return nil, err + } + + return &user, nil +} + func FindOrSaveBaseUser(dbConn *sql.DB, user *User) (*User, error) { log.Println("finding or saving user", user.ID) @@ -91,7 +92,7 @@ func FindOrSaveBaseUser(dbConn *sql.DB, user *User) (*User, error) { func SaveUser(dbConn *sql.DB, user *User) (*User, error) { log.Println("saving user", user.ID) - _, err := dbConn.Exec(`INSERT OR REPLACE INTO users (id, mail, username, display_name, bio, location, website, avatar) VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, user.ID, user.Mail, user.Username, user.DisplayName, user.Bio, user.Location, user.Website, user.Avatar) + _, 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 } 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/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 @@

generate key.


- {{ if .FormError }} - {{ if (len .FormError.Errors) }} - {{ range $error := .FormError.Errors }} -
{{ $error }}
- {{ end }} - {{ end }} - {{ end }} {{ end }} diff --git a/templates/base.html b/templates/base.html index 285b0dc..89d6dd2 100644 --- a/templates/base.html +++ b/templates/base.html @@ -48,6 +48,17 @@
+ {{ if and .Success (gt (len .Success.Messages) 0) }} + {{ range $message := .Success.Messages }} +
{{ $message }}
+ {{ end }} + {{ end }} + {{ if and .Error (gt (len .Error.Messages) 0) }} + {{ range $error := .Error.Messages }} +
{{ $error }}
+ {{ end }} + {{ end }} + {{ template "content" . }}
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 }} /> - - - - - {{ if .FormError }} - {{ if (len .FormError.Errors) }} - {{ range $error := .FormError.Errors }} -
{{ $error }}
- {{ end }} - {{ end }} - {{ end }} + - - {{ 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 @@

-
- {{ if .FormError }} - {{ if (len .FormError.Errors) }} - {{ range $error := .FormError.Errors }} -
{{ $error }}
- {{ end }} - {{ end }} - {{ end }}
diff --git a/templates/home.html b/templates/home.html index 09edc20..76bbc6a 100644 --- a/templates/home.html +++ b/templates/home.html @@ -1,8 +1,19 @@ {{ define "content" }} -

hello there!

-

current peeps:

+

hello there!

+

current peeps in the club :D

+
{{ range $user := .Users }} -

{{ $user.Username }}

- {{ $user.Username }} +
+
+
+
+
+
name: {{ $user.Username }}
+
pronouns: {{ $user.Pronouns }}
+
{{ $user.Website }}
+
{{ $user.Bio }}
+
+
{{ end }} +
{{ end }} diff --git a/templates/profile.html b/templates/profile.html index 52f7127..a6e1b68 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -3,27 +3,22 @@

hey {{ .Profile.DisplayName }}


- - + + - + - + - + + + + - - {{ if .FormError }} - {{ if (len .FormError.Errors) }} - {{ range $error := .FormError.Errors }} -
{{ $error }}
- {{ end }} - {{ end }} - {{ end }}
{{ end }} diff --git a/uploads/avatars/f54386d9-f310-4c5a-a3f9-e950320479f7 b/uploads/avatars/f54386d9-f310-4c5a-a3f9-e950320479f7 deleted file mode 100644 index 60b7a10..0000000 Binary files a/uploads/avatars/f54386d9-f310-4c5a-a3f9-e950320479f7 and /dev/null differ