Compare commits

..

No commits in common. "a32026d0133b868840d97b6b573310143c897269" and "3d7bb77eab51ad369f6c7050ff37fdfb488b1467" have entirely different histories.

25 changed files with 212 additions and 343 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
.env .env
hatecomputers.club hatecomputers.club
*.db *.db
uploads

View File

@ -170,7 +170,6 @@ func VerifySessionContinuation(context *types.RequestContext, req *http.Request,
func GoLoginContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { func GoLoginContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
log.Println("GoLoginContinuation")
http.SetCookie(resp, &http.Cookie{ http.SetCookie(resp, &http.Cookie{
Name: "redirect", Name: "redirect",
Value: req.URL.Path, Value: req.URL.Path,

View File

@ -18,6 +18,34 @@ const MaxUserRecords = 100
var UserOwnedInternalFmtDomains = []string{"%s", "%s.endpoints"} 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 { func ListDNSRecordsContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID) dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID)
@ -35,8 +63,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 { 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(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
formErrors := types.BannerMessages{ formErrors := types.FormError{
Messages: []string{}, Errors: []string{},
} }
internal := req.FormValue("internal") == "on" || req.FormValue("internal") == "true" internal := req.FormValue("internal") == "on" || req.FormValue("internal") == "true"
@ -53,7 +81,7 @@ func CreateDNSRecordContinuation(dnsAdapter external_dns.ExternalDNSAdapter, max
ttlNum, err := strconv.Atoi(ttl) ttlNum, err := strconv.Atoi(ttl)
if err != nil { if err != nil {
resp.WriteHeader(http.StatusBadRequest) resp.WriteHeader(http.StatusBadRequest)
formErrors.Messages = append(formErrors.Messages, "invalid ttl") formErrors.Errors = append(formErrors.Errors, "invalid ttl")
} }
dnsRecordCount, err := database.CountUserDNSRecords(context.DBConn, context.User.ID) dnsRecordCount, err := database.CountUserDNSRecords(context.DBConn, context.User.ID)
@ -64,7 +92,7 @@ func CreateDNSRecordContinuation(dnsAdapter external_dns.ExternalDNSAdapter, max
} }
if dnsRecordCount >= maxUserRecords { if dnsRecordCount >= maxUserRecords {
resp.WriteHeader(http.StatusTooManyRequests) resp.WriteHeader(http.StatusTooManyRequests)
formErrors.Messages = append(formErrors.Messages, "max records reached") formErrors.Errors = append(formErrors.Errors, "max records reached")
} }
dnsRecord := &database.DNSRecord{ dnsRecord := &database.DNSRecord{
@ -78,10 +106,10 @@ func CreateDNSRecordContinuation(dnsAdapter external_dns.ExternalDNSAdapter, max
if !userCanFuckWithDNSRecord(context.DBConn, context.User, dnsRecord, allowedUserDomainFormats) { if !userCanFuckWithDNSRecord(context.DBConn, context.User, dnsRecord, allowedUserDomainFormats) {
resp.WriteHeader(http.StatusUnauthorized) resp.WriteHeader(http.StatusUnauthorized)
formErrors.Messages = append(formErrors.Messages, "'name' must end with "+context.User.Username+" or you must be a domain owner for internal domains") formErrors.Errors = append(formErrors.Errors, "'name' must end with "+context.User.Username+" or you must be a domain owner for internal domains")
} }
if len(formErrors.Messages) == 0 { if len(formErrors.Errors) == 0 {
if dnsRecord.Internal { if dnsRecord.Internal {
dnsRecord.ID = utils.RandomId() dnsRecord.ID = utils.RandomId()
} else { } else {
@ -89,28 +117,24 @@ func CreateDNSRecordContinuation(dnsAdapter external_dns.ExternalDNSAdapter, max
if err != nil { if err != nil {
log.Println(err) log.Println(err)
resp.WriteHeader(http.StatusInternalServerError) resp.WriteHeader(http.StatusInternalServerError)
formErrors.Messages = append(formErrors.Messages, err.Error()) formErrors.Errors = append(formErrors.Errors, err.Error())
} }
} }
} }
if len(formErrors.Messages) == 0 { if len(formErrors.Errors) == 0 {
_, err := database.SaveDNSRecord(context.DBConn, dnsRecord) _, err := database.SaveDNSRecord(context.DBConn, dnsRecord)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
formErrors.Messages = append(formErrors.Messages, "error saving record") formErrors.Errors = append(formErrors.Errors, "error saving record")
} }
} }
if len(formErrors.Messages) == 0 { if len(formErrors.Errors) == 0 {
formSuccess := types.BannerMessages{
Messages: []string{"record added."},
}
(*context.TemplateData)["Success"] = formSuccess
return success(context, req, resp) return success(context, req, resp)
} }
(*context.TemplateData)[""] = &formErrors (*context.TemplateData)["FormError"] = &formErrors
(*context.TemplateData)["RecordForm"] = dnsRecord (*context.TemplateData)["RecordForm"] = dnsRecord
return failure(context, req, resp) return failure(context, req, resp)
} }
@ -148,39 +172,7 @@ func DeleteDNSRecordContinuation(dnsAdapter external_dns.ExternalDNSAdapter) fun
return failure(context, req, resp) return failure(context, req, resp)
} }
formSuccess := types.BannerMessages{
Messages: []string{"record deleted."},
}
(*context.TemplateData)["Success"] = formSuccess
return success(context, req, resp) 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
}

View File

@ -10,60 +10,6 @@ import (
"git.hatecomputers.club/hatecomputers/hatecomputers.club/utils" "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 { func validateGuestbookEntry(entry *database.GuestbookEntry) []string {
errors := []string{} errors := []string{}
@ -87,3 +33,53 @@ func validateGuestbookEntry(entry *database.GuestbookEntry) []string {
return errors 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)
}
}

View File

@ -62,8 +62,8 @@ func CaptchaVerificationContinuation(context *types.RequestContext, req *http.Re
err := verifyCaptcha(secretKey, hCaptchaResponse) err := verifyCaptcha(secretKey, hCaptchaResponse)
if err != nil { if err != nil {
(*context.TemplateData)["Error"] = types.BannerMessages{ (*context.TemplateData)["FormError"] = types.FormError{
Messages: []string{"hCaptcha verification failed"}, Errors: []string{"hCaptcha verification failed"},
} }
resp.WriteHeader(http.StatusBadRequest) resp.WriteHeader(http.StatusBadRequest)

View File

@ -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 { func CreateAPIKeyContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
formErrors := types.BannerMessages{ formErrors := types.FormError{
Messages: []string{}, Errors: []string{},
} }
numKeys, err := database.CountUserAPIKeys(context.DBConn, context.User.ID) 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 { if numKeys >= MAX_USER_API_KEYS {
formErrors.Messages = append(formErrors.Messages, "max types keys reached") formErrors.Errors = append(formErrors.Errors, "max types keys reached")
} }
if len(formErrors.Messages) > 0 { if len(formErrors.Errors) > 0 {
(*context.TemplateData)["Error"] = formErrors (*context.TemplateData)["FormError"] = formErrors
return failure(context, req, resp) return failure(context, req, resp)
} }
@ -56,11 +56,6 @@ func CreateAPIKeyContinuation(context *types.RequestContext, req *http.Request,
resp.WriteHeader(http.StatusInternalServerError) resp.WriteHeader(http.StatusInternalServerError)
return failure(context, req, resp) return failure(context, req, resp)
} }
formSuccess := types.BannerMessages{
Messages: []string{"key created."},
}
(*context.TemplateData)["Success"] = formSuccess
return success(context, req, resp) return success(context, req, resp)
} }
} }
@ -87,11 +82,6 @@ func DeleteAPIKeyContinuation(context *types.RequestContext, req *http.Request,
return failure(context, req, resp) return failure(context, req, resp)
} }
formSuccess := types.BannerMessages{
Messages: []string{"key deleted."},
}
(*context.TemplateData)["Success"] = formSuccess
return success(context, req, resp) return success(context, req, resp)
} }
} }

View File

@ -3,7 +3,6 @@ package profiles
import ( import (
"log" "log"
"net/http" "net/http"
"strings"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/files" "git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/files"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/types" "git.hatecomputers.club/hatecomputers/hatecomputers.club/api/types"
@ -28,91 +27,71 @@ 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 { 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(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
formErrors := types.BannerMessages{ formErrors := types.FormError{
Messages: []string{}, Errors: []string{},
} }
err := req.ParseMultipartForm(int64(maxAvatarSize)) err := req.ParseMultipartForm(int64(maxAvatarSize))
if err != nil { if err != nil {
formErrors.Messages = append(formErrors.Messages, "avatar file too large") log.Println(err)
formErrors.Errors = append(formErrors.Errors, "avatar file too large")
} }
if len(formErrors.Messages) == 0 { if len(formErrors.Errors) == 0 {
file, _, err := req.FormFile("avatar") file, _, err := req.FormFile("avatar")
if file != nil && err != nil { if file == nil {
formErrors.Messages = append(formErrors.Messages, "error uploading avatar") formErrors.Errors = append(formErrors.Errors, "avatar required")
} else if file != nil { }
if err != nil {
formErrors.Errors = append(formErrors.Errors, "error uploading avatar")
} else {
defer file.Close() defer file.Close()
reader := http.MaxBytesReader(resp, file, int64(maxAvatarSize))
defer reader.Close()
_, err = fileAdapter.CreateFile(avatarPath+context.User.ID, reader) _, err = fileAdapter.CreateFile(avatarPath+context.User.ID, file)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
formErrors.Messages = append(formErrors.Messages, "error saving avatar (is it too big?)") formErrors.Errors = append(formErrors.Errors, "error saving avatar")
} }
} }
} }
context.User.Bio = strings.Trim(req.FormValue("bio"), "\n") bio := req.FormValue("bio")
context.User.Pronouns = req.FormValue("pronouns") location := req.FormValue("location")
context.User.Location = req.FormValue("location") website := req.FormValue("website")
context.User.Website = req.FormValue("website") if len(bio) > 128 {
context.User.Avatar = avatarPrefix + context.User.ID formErrors.Errors = append(formErrors.Errors, "bio too long, keep it to 128")
formErrors.Messages = append(formErrors.Messages, validateProfileUpdate(context.User)...) }
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
if len(formErrors.Messages) == 0 {
_, err = database.SaveUser(context.DBConn, context.User) _, err = database.SaveUser(context.DBConn, context.User)
if err != nil { if err != nil {
formErrors.Messages = append(formErrors.Messages, "error saving profile") formErrors.Errors = append(formErrors.Errors, "error saving profile")
} }
} }
(*context.TemplateData)["Profile"] = context.User (*context.TemplateData)["Profile"] = context.User
(*context.TemplateData)["Error"] = formErrors (*context.TemplateData)["FormError"] = formErrors
if len(formErrors.Messages) > 0 { if len(formErrors.Errors) > 0 {
log.Println(formErrors.Messages) log.Println(formErrors.Errors)
resp.WriteHeader(http.StatusBadRequest) resp.WriteHeader(http.StatusBadRequest)
return failure(context, req, resp) return failure(context, req, resp)
} }
formSuccess := types.BannerMessages{
Messages: []string{"profile updated"},
}
(*context.TemplateData)["Success"] = formSuccess
return success(context, req, resp) 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
}

View File

@ -71,11 +71,16 @@ func CacheControlMiddleware(next http.Handler, maxAge int) http.Handler {
func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server { func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
mux := http.NewServeMux() mux := http.NewServeMux()
// "dependency injection" 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)))
cloudflareAdapter := &cloudflare.CloudflareExternalDNSAdapter{ cloudflareAdapter := &cloudflare.CloudflareExternalDNSAdapter{
APIToken: argv.CloudflareToken, APIToken: argv.CloudflareToken,
ZoneId: argv.CloudflareZone, ZoneId: argv.CloudflareZone,
} }
uploadAdapter := &filesystem.FilesystemAdapter{ uploadAdapter := &filesystem.FilesystemAdapter{
BasePath: argv.UploadPath, BasePath: argv.UploadPath,
Permissions: 0777, Permissions: 0777,
@ -89,11 +94,6 @@ 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) { mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
requestContext := makeRequestContext() requestContext := makeRequestContext()
LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(auth.ListUsersContinuation, auth.ListUsersContinuation)(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)
@ -126,9 +126,7 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
mux.HandleFunc("POST /profile", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("POST /profile", func(w http.ResponseWriter, r *http.Request) {
requestContext := makeRequestContext() requestContext := makeRequestContext()
updateProfileContinuation := profiles.UpdateProfileContinuation(uploadAdapter, profiles.MaxAvatarSize, profiles.AvatarPath, profiles.AvatarPrefix) 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)
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) { mux.HandleFunc("GET /dns", func(w http.ResponseWriter, r *http.Request) {

View File

@ -20,8 +20,8 @@ type RequestContext struct {
User *database.User User *database.User
} }
type BannerMessages struct { type FormError struct {
Messages []string Errors []string
} }
type Continuation func(*RequestContext, *http.Request, http.ResponseWriter) ContinuationChain type Continuation func(*RequestContext, *http.Request, http.ResponseWriter) ContinuationChain

View File

@ -4,7 +4,6 @@ import (
"log" "log"
"database/sql" "database/sql"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
@ -145,7 +144,6 @@ func MigrateProfiles(dbConn *sql.DB) (*sql.DB, error) {
} }
columns := map[string]string{} columns := map[string]string{}
columns["pronouns"] = "unspecified"
columns["bio"] = "a computer hater" columns["bio"] = "a computer hater"
columns["location"] = "earth" columns["location"] = "earth"
columns["website"] = "https://hatecomputers.club" columns["website"] = "https://hatecomputers.club"

View File

@ -27,7 +27,6 @@ type User struct {
Bio string `json:"bio"` Bio string `json:"bio"`
Location string `json:"location"` Location string `json:"location"`
Website string `json:"website"` Website string `json:"website"`
Pronouns string `json:"pronouns"` // liberals!! :O
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
} }
@ -38,10 +37,25 @@ type UserSession struct {
ExpireAt time.Time `json:"expire_at"` 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) { func ListUsers(dbConn *sql.DB) ([]*User, error) {
log.Println("listing users") log.Println("listing users")
rows, err := dbConn.Query(`SELECT id, mail, username, display_name, bio, location, website, avatar, pronouns, created_at FROM users;`) rows, err := dbConn.Query(`SELECT id, mail, username, display_name, bio, location, website, avatar, created_at FROM users;`)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return nil, err return nil, err
@ -51,7 +65,7 @@ func ListUsers(dbConn *sql.DB) ([]*User, error) {
var users []*User var users []*User
for rows.Next() { for rows.Next() {
var user User 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) err := rows.Scan(&user.ID, &user.Mail, &user.Username, &user.DisplayName, &user.Bio, &user.Location, &user.Website, &user.Avatar, &user.CreatedAt)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return nil, err return nil, err
@ -63,21 +77,6 @@ func ListUsers(dbConn *sql.DB) ([]*User, error) {
return users, nil 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) { func FindOrSaveBaseUser(dbConn *sql.DB, user *User) (*User, error) {
log.Println("finding or saving user", user.ID) log.Println("finding or saving user", user.ID)
@ -92,7 +91,7 @@ func FindOrSaveBaseUser(dbConn *sql.DB, user *User) (*User, error) {
func SaveUser(dbConn *sql.DB, user *User) (*User, error) { func SaveUser(dbConn *sql.DB, user *User) (*User, error) {
log.Println("saving user", user.ID) 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) _, 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)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,48 +0,0 @@
.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;
}

View File

@ -1,8 +1,7 @@
:root { :root {
--background-color-light: #f4e8e9; --background-color-light: #f4e8e9;
--background-color-light-2: #f5e6f3; --background-color-light-2: #f7f7f7;
--text-color-light: #333; --text-color-light: #333;
--confirm-color-light: #91d9bb;
--link-color-light: #d291bc; --link-color-light: #d291bc;
--container-bg-light: #fff7f87a; --container-bg-light: #fff7f87a;
--border-color-light: #692fcc; --border-color-light: #692fcc;
@ -11,7 +10,6 @@
--background-color-dark: #333; --background-color-dark: #333;
--background-color-dark-2: #2c2c2c; --background-color-dark-2: #2c2c2c;
--text-color-dark: #f4e8e9; --text-color-dark: #f4e8e9;
--confirm-color-dark: #4d8f73;
--link-color-dark: #b86b77; --link-color-dark: #b86b77;
--container-bg-dark: #424242ea; --container-bg-dark: #424242ea;
--border-color-dark: #956ade; --border-color-dark: #956ade;
@ -26,7 +24,6 @@
--container-bg: var(--container-bg-dark); --container-bg: var(--container-bg-dark);
--border-color: var(--border-color-dark); --border-color: var(--border-color-dark);
--error-color: var(--error-color-dark); --error-color: var(--error-color-dark);
--confirm-color: var(--confirm-color-dark);
} }
[data-theme="LIGHT"] { [data-theme="LIGHT"] {
@ -37,15 +34,9 @@
--container-bg: var(--container-bg-light); --container-bg: var(--container-bg-light);
--border-color: var(--border-color-light); --border-color: var(--border-color-light);
--error-color: var(--error-color-light); --error-color: var(--error-color-light);
--confirm-color: var(--confirm-color-light);
} }
.error { .error {
background-color: var(--error-color); background-color: var(--error-color);
padding: 1rem; padding: 1rem;
} }
.success {
background-color: var(--confirm-color);
padding: 1rem;
}

View File

@ -36,7 +36,4 @@ textarea {
margin: 0 0 1em; margin: 0 0 1em;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
background: var(--container-bg); background: var(--container-bg);
resize: vertical;
min-height: 100px;
} }

View File

@ -3,7 +3,6 @@
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
padding: 10px; padding: 10px;
max-width: 700px;
} }
.entry-name { .entry-name {

View File

@ -3,7 +3,6 @@
@import "/static/css/table.css"; @import "/static/css/table.css";
@import "/static/css/form.css"; @import "/static/css/form.css";
@import "/static/css/guestbook.css"; @import "/static/css/guestbook.css";
@import "/static/css/club.css";
@font-face { @font-face {
font-family: "ComicSans"; font-family: "ComicSans";
@ -23,27 +22,15 @@
} }
@-webkit-keyframes cursor { @-webkit-keyframes cursor {
0% { 0% {cursor: url("/static/img/cursor-2.png"), auto;}
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;}
50% {
cursor: url("/static/img/cursor-1.png"), auto;
}
100% {
cursor: url("/static/img/cursor-2.png"), auto;
}
} }
@keyframes cursor { @keyframes cursor {
0% { 0% {cursor: url("/static/img/cursor-2.png"), auto;}
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;}
50% {
cursor: url("/static/img/cursor-1.png"), auto;
}
100% {
cursor: url("/static/img/cursor-2.png"), auto;
}
} }
body { body {
@ -83,14 +70,3 @@ hr {
max-width: 900px; max-width: 900px;
gap: 10px 10px; gap: 10px 10px;
} }
.info {
margin-bottom: 1rem;
max-width: 600px;
transition: opacity 0.3s;
}
.info:hover {
opacity: 0.8;
}

View File

@ -1,6 +0,0 @@
const infoBanners = document.querySelectorAll(".info");
Array.from(infoBanners).forEach((infoBanner) => {
infoBanner.addEventListener("click", () => {
infoBanner.remove();
});
});

View File

@ -1,6 +1,5 @@
const scripts = [ const scripts = [
"/static/js/components/themeSwitcher.js", "/static/js/components/themeSwitcher.js",
"/static/js/components/formatDate.js", "/static/js/components/formatDate.js",
"/static/js/components/infoBanners.js",
]; ];
requirejs(scripts); requirejs(scripts);

View File

@ -28,5 +28,12 @@
<h2>generate key.</h2> <h2>generate key.</h2>
<hr> <hr>
<input type="submit" value="generate." /> <input type="submit" value="generate." />
{{ if .FormError }}
{{ if (len .FormError.Errors) }}
{{ range $error := .FormError.Errors }}
<div class="error">{{ $error }}</div>
{{ end }}
{{ end }}
{{ end }}
</form> </form>
{{ end }} {{ end }}

View File

@ -48,17 +48,6 @@
<hr> <hr>
<div id="content"> <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" . }} {{ template "content" . }}
</div> </div>

View File

@ -76,6 +76,18 @@
{{ end }} {{ end }}
/> />
</label> </label>
<input type="submit" value="add." />
<input type="submit" value="Add" />
{{ if .FormError }}
{{ if (len .FormError.Errors) }}
{{ range $error := .FormError.Errors }}
<div class="error">{{ $error }}</div>
{{ end }}
{{ end }}
{{ end }}
</form> </form>
{{ end }} {{ end }}

View File

@ -21,9 +21,18 @@
<div <div
class="h-captcha" class="h-captcha"
data-sitekey="{{ .HcaptchaArgs.SiteKey }}" data-sitekey="{{ .HcaptchaArgs.SiteKey }}"
data-theme="dark"
></div> ></div>
<br> <br>
<button type="submit" class="btn btn-primary">sign.</button> <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> </form>
<hr> <hr>

View File

@ -1,19 +1,8 @@
{{ define "content" }} {{ define "content" }}
<h2 class="blinky">hello there!</h2> <h2 class="blinky">hello there!</h2>
<p>current peeps in the club :D</p> <p>current peeps:</p>
<div class="club-members">
{{ range $user := .Users }} {{ range $user := .Users }}
<div class="club-member"> <p>{{ $user.Username }}</p>
<div class="avatar"> <img src="{{ $user.Avatar }}" alt="{{ $user.Username }}" />
<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 }} {{ end }}
</div>
{{ end }} {{ end }}

View File

@ -3,22 +3,27 @@
<h1>hey {{ .Profile.DisplayName }}</h1> <h1>hey {{ .Profile.DisplayName }}</h1>
<br> <br>
<form action="/profile" method="POST" class="form" enctype="multipart/form-data"> <form action="/profile" method="POST" class="form" enctype="multipart/form-data">
<label for="file" class="file-upload">avatar.</label> <label for="type">avatar.</label>
<input type="file" name="avatar"> <input type="file" name="avatar" required>
<label for="location">location.</label> <label for="type">location.</label>
<input type="text" name="location" value="{{ .Profile.Location }}"> <input type="text" name="location" value="{{ .Profile.Location }}">
<label for="website">website.</label> <label for="type">website.</label>
<input type="text" name="website" value="{{ .Profile.Website }}"> <input type="text" name="website" value="{{ .Profile.Website }}">
<label for="pronouns">pronouns.</label> <label for="type">bio.</label>
<input type="text" name="pronouns" value="{{ .Profile.Pronouns }}">
<label for="bio">bio.</label>
<textarea name="bio">{{ .Profile.Bio }}</textarea> <textarea name="bio">{{ .Profile.Bio }}</textarea>
<input type="submit" value="update"> <input type="submit" value="update">
{{ if .FormError }}
{{ if (len .FormError.Errors) }}
{{ range $error := .FormError.Errors }}
<div class="error">{{ $error }}</div>
{{ end }}
{{ end }}
{{ end }}
</form> </form>
{{ end }} {{ end }}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB