add error and success messages, finish profile validation
	
		
			
	
		
	
	
		
			
				
	
				continuous-integration/drone/pr Build was killed
				
					Details
				
			
		
	
				
					
				
			
				
	
				continuous-integration/drone/pr Build was killed
				
					Details
				
			
		
	This commit is contained in:
		
							parent
							
								
									fb85836649
								
							
						
					
					
						commit
						a32026d013
					
				|  | @ -1,3 +1,4 @@ | |||
| .env | ||||
| hatecomputers.club | ||||
| *.db | ||||
| uploads | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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 | ||||
| 	} | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -36,4 +36,7 @@ textarea { | |||
|   margin: 0 0 1em; | ||||
|   border: 1px solid var(--border-color); | ||||
|   background: var(--container-bg); | ||||
| 
 | ||||
|   resize: vertical; | ||||
|   min-height: 100px; | ||||
| } | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ | |||
|   border: 1px solid var(--border-color); | ||||
| 
 | ||||
|   padding: 10px; | ||||
|   max-width: 700px; | ||||
| } | ||||
| 
 | ||||
| .entry-name { | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,6 @@ | |||
| const infoBanners = document.querySelectorAll(".info"); | ||||
| Array.from(infoBanners).forEach((infoBanner) => { | ||||
|   infoBanner.addEventListener("click", () => { | ||||
|     infoBanner.remove(); | ||||
|   }); | ||||
| }); | ||||
|  | @ -1,5 +1,6 @@ | |||
| const scripts = [ | ||||
|   "/static/js/components/themeSwitcher.js", | ||||
|   "/static/js/components/formatDate.js", | ||||
|   "/static/js/components/infoBanners.js", | ||||
| ]; | ||||
| requirejs(scripts); | ||||
|  |  | |||
|  | @ -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 }} | ||||
|  |  | |||
|  | @ -48,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> | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 }} | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -1,8 +1,19 @@ | |||
| {{ define "content" }} | ||||
|   <h2 class="blinky">hello there!</h2> | ||||
|   <p>current peeps:</p> | ||||
| <h2 class="blinky">hello there!</h2> | ||||
| <p>current peeps in the club :D</p> | ||||
| <div class="club-members"> | ||||
|   {{ range $user := .Users }} | ||||
|     <p>{{ $user.Username }}</p> | ||||
|     <img src="{{ $user.Avatar }}" alt="{{ $user.Username }}" /> | ||||
|   <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 }} | ||||
|  |  | |||
|  | @ -3,27 +3,22 @@ | |||
| <h1>hey {{ .Profile.DisplayName }}</h1> | ||||
| <br> | ||||
| <form action="/profile" method="POST" class="form" enctype="multipart/form-data"> | ||||
|   <label for="type">avatar.</label> | ||||
|   <input type="file" name="avatar" required> | ||||
|   <label for="file" class="file-upload">avatar.</label> | ||||
|   <input type="file" name="avatar"> | ||||
| 
 | ||||
|   <label for="type">location.</label> | ||||
|   <label for="location">location.</label> | ||||
|   <input type="text" name="location" value="{{ .Profile.Location }}"> | ||||
| 
 | ||||
|   <label for="type">website.</label> | ||||
|   <label for="website">website.</label> | ||||
|   <input type="text" name="website" value="{{ .Profile.Website }}"> | ||||
| 
 | ||||
|   <label for="type">bio.</label> | ||||
|   <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"> | ||||
| 
 | ||||
|   {{ if .FormError }} | ||||
|   {{ if (len .FormError.Errors) }} | ||||
|   {{ range $error := .FormError.Errors }} | ||||
|   <div class="error">{{ $error }}</div> | ||||
|   {{ end }} | ||||
|   {{ end }} | ||||
|   {{ end }} | ||||
| </form> | ||||
| 
 | ||||
| {{ end }} | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 28 KiB | 
		Loading…
	
		Reference in New Issue