get something up there
This commit is contained in:
		
							parent
							
								
									ee49015cc9
								
							
						
					
					
						commit
						eaa7cc8547
					
				|  | @ -11,4 +11,4 @@ RUN go build -o /app/hatecomputers | |||
| 
 | ||||
| EXPOSE 8080 | ||||
| 
 | ||||
| CMD ["/app/hatecomputers", "--server", "--migrate", "--port", "8080", "--template-path", "/app/templates", "--database-path", "/app/db/hatecomputers.db", "--static-path", "/app/static", "--scheduler", "--dns", "--dns-port", "8053", "--dns-resolvers", "1.1.1.1:53,1.0.0.1:53"] | ||||
| CMD ["/app/hatecomputers", "--server", "--migrate", "--port", "8080", "--template-path", "/app/templates", "--database-path", "/app/db/hatecomputers.db", "--static-path", "/app/static", "--scheduler", "--dns", "--dns-port", "8053", "--dns-resolvers", "1.1.1.1:53,1.0.0.1:53", "--uploads", "/app/uploads"] | ||||
|  |  | |||
|  | @ -0,0 +1,8 @@ | |||
| package files | ||||
| 
 | ||||
| import "io" | ||||
| 
 | ||||
| type FilesAdapter interface { | ||||
| 	CreateFile(path string, content io.Reader) (string, error) | ||||
| 	DeleteFile(path string) error | ||||
| } | ||||
|  | @ -0,0 +1,37 @@ | |||
| package filesystem | ||||
| 
 | ||||
| import ( | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| ) | ||||
| 
 | ||||
| type FilesystemAdapter struct { | ||||
| 	BasePath    string | ||||
| 	Permissions os.FileMode | ||||
| } | ||||
| 
 | ||||
| func (f *FilesystemAdapter) CreateFile(path string, content io.Reader) (string, error) { | ||||
| 	fullPath := f.BasePath + path | ||||
| 	dir := filepath.Dir(fullPath) | ||||
| 	if _, err := os.Stat(dir); os.IsNotExist(err) { | ||||
| 		os.MkdirAll(dir, f.Permissions) | ||||
| 	} | ||||
| 
 | ||||
| 	file, err := os.Create(f.BasePath + path) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	defer file.Close() | ||||
| 
 | ||||
| 	_, err = io.Copy(file, content) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	return path, nil | ||||
| } | ||||
| 
 | ||||
| func (f *FilesystemAdapter) DeleteFile(path string) error { | ||||
| 	return os.Remove(f.BasePath + path) | ||||
| } | ||||
|  | @ -18,6 +18,18 @@ import ( | |||
| 	"golang.org/x/oauth2" | ||||
| ) | ||||
| 
 | ||||
| func ListUsersContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { | ||||
| 	return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { | ||||
| 		users, err := database.ListUsers(context.DBConn) | ||||
| 		if err != nil { | ||||
| 			return failure(context, req, resp) | ||||
| 		} | ||||
| 
 | ||||
| 		(*context.TemplateData)["Users"] = users | ||||
| 		return success(context, req, resp) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func StartSessionContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { | ||||
| 	return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { | ||||
| 		verifier := utils.RandomId() + utils.RandomId() | ||||
|  | @ -216,7 +228,7 @@ func getOauthUser(dbConn *sql.DB, client *http.Client, uri string) (*database.Us | |||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	user, err := database.FindOrSaveUser(dbConn, userStruct) | ||||
| 	user, err := database.FindOrSaveBaseUser(dbConn, userStruct) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  |  | |||
|  | @ -8,12 +8,16 @@ import ( | |||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/external_dns" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/types" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/database" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/utils" | ||||
| ) | ||||
| 
 | ||||
| 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 { | ||||
|  |  | |||
|  | @ -39,7 +39,7 @@ func setup() (*sql.DB, *types.RequestContext, func()) { | |||
| 		Mail:        "test@test.com", | ||||
| 		DisplayName: "test", | ||||
| 	} | ||||
| 	database.FindOrSaveUser(testDb, user) | ||||
| 	database.FindOrSaveBaseUser(testDb, user) | ||||
| 
 | ||||
| 	context := &types.RequestContext{ | ||||
| 		DBConn:       testDb, | ||||
|  | @ -246,7 +246,7 @@ func TestThatUserMustOwnRecordToRemove(t *testing.T) { | |||
| 	defer testServer.Close() | ||||
| 
 | ||||
| 	nonOwnerUser := &database.User{ID: "n/a", Username: "testuser"} | ||||
| 	_, err := database.FindOrSaveUser(db, nonOwnerUser) | ||||
| 	_, err := database.FindOrSaveBaseUser(db, nonOwnerUser) | ||||
| 	if err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -0,0 +1,83 @@ | |||
| package profiles | ||||
| 
 | ||||
| import ( | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/files" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/types" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/database" | ||||
| ) | ||||
| 
 | ||||
| const MaxAvatarSize = 1024 * 1024 * 2 // 2MB
 | ||||
| const AvatarPath = "avatars/" | ||||
| 
 | ||||
| func GetProfileContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { | ||||
| 	return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { | ||||
| 		if context.User == nil { | ||||
| 			return failure(context, req, resp) | ||||
| 		} | ||||
| 
 | ||||
| 		(*context.TemplateData)["Profile"] = context.User | ||||
| 		return success(context, req, resp) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func UpdateProfileContinuation(fileAdapter files.FilesAdapter, maxAvatarSize int, avatarPath string) 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{}, | ||||
| 			} | ||||
| 
 | ||||
| 			err := req.ParseMultipartForm(int64(maxAvatarSize)) | ||||
| 			if err != nil { | ||||
| 				log.Println(err) | ||||
| 
 | ||||
| 				formErrors.Errors = append(formErrors.Errors, "avatar file too large") | ||||
| 			} | ||||
| 
 | ||||
| 			if len(formErrors.Errors) == 0 { | ||||
| 				file, _, err := req.FormFile("avatar") | ||||
| 				if err != nil { | ||||
| 					formErrors.Errors = append(formErrors.Errors, "error uploading avatar") | ||||
| 				} else { | ||||
| 					defer file.Close() | ||||
| 
 | ||||
| 					_, err = fileAdapter.CreateFile(avatarPath+context.User.ID, file) | ||||
| 					if err != nil { | ||||
| 						log.Println(err) | ||||
| 						formErrors.Errors = append(formErrors.Errors, "error saving avatar") | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			if len(formErrors.Errors) == 0 { | ||||
| 				bio := req.FormValue("bio") | ||||
| 				location := req.FormValue("location") | ||||
| 				website := req.FormValue("website") | ||||
| 
 | ||||
| 				context.User.Bio = bio | ||||
| 				context.User.Location = location | ||||
| 				context.User.Website = website | ||||
| 
 | ||||
| 				_, err = database.SaveUser(context.DBConn, context.User) | ||||
| 				if err != nil { | ||||
| 					formErrors.Errors = append(formErrors.Errors, "error saving profile") | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			(*context.TemplateData)["Profile"] = context.User | ||||
| 			(*context.TemplateData)["FormError"] = formErrors | ||||
| 
 | ||||
| 			if len(formErrors.Errors) > 0 { | ||||
| 				log.Println(formErrors.Errors) | ||||
| 
 | ||||
| 				resp.WriteHeader(http.StatusBadRequest) | ||||
| 				return failure(context, req, resp) | ||||
| 			} | ||||
| 
 | ||||
| 			return success(context, req, resp) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										32
									
								
								api/serve.go
								
								
								
								
							
							
						
						
									
										32
									
								
								api/serve.go
								
								
								
								
							|  | @ -7,12 +7,14 @@ import ( | |||
| 	"net/http" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/cloudflare" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/external_dns/cloudflare" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/files/filesystem" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/auth" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/dns" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/guestbook" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/hcaptcha" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/keys" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/profiles" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/template" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/types" | ||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/args" | ||||
|  | @ -32,7 +34,6 @@ func LogRequestContinuation(context *types.RequestContext, req *http.Request, re | |||
| func LogExecutionTimeContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { | ||||
| 	return func(success types.Continuation, _failure types.Continuation) types.ContinuationChain { | ||||
| 		end := time.Now() | ||||
| 
 | ||||
| 		log.Println(context.Id, "took", end.Sub(context.Start)) | ||||
| 
 | ||||
| 		return success(context, req, resp) | ||||
|  | @ -70,14 +71,21 @@ func CacheControlMiddleware(next http.Handler, maxAge int) http.Handler { | |||
| func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server { | ||||
| 	mux := http.NewServeMux() | ||||
| 
 | ||||
| 	fileServer := http.FileServer(http.Dir(argv.StaticPath)) | ||||
| 	mux.Handle("GET /static/", http.StripPrefix("/static/", CacheControlMiddleware(fileServer, 3600))) | ||||
| 	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{ | ||||
| 		APIToken: argv.CloudflareToken, | ||||
| 		ZoneId:   argv.CloudflareZone, | ||||
| 	} | ||||
| 
 | ||||
| 	uploadAdapter := &filesystem.FilesystemAdapter{ | ||||
| 		BasePath:    argv.UploadPath, | ||||
| 		Permissions: 0777, | ||||
| 	} | ||||
| 
 | ||||
| 	makeRequestContext := func() *types.RequestContext { | ||||
| 		return &types.RequestContext{ | ||||
| 			DBConn:       dbConn, | ||||
|  | @ -88,7 +96,7 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server { | |||
| 
 | ||||
| 	mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		requestContext := makeRequestContext() | ||||
| 		LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(template.TemplateContinuation("home.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) | ||||
| 		LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(auth.ListUsersContinuation, auth.ListUsersContinuation)(template.TemplateContinuation("home.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) | ||||
| 	}) | ||||
| 
 | ||||
| 	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { | ||||
|  | @ -111,16 +119,24 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server { | |||
| 		LogRequestContinuation(requestContext, r, w)(auth.LogoutContinuation, FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) | ||||
| 	}) | ||||
| 
 | ||||
| 	mux.HandleFunc("GET /profile", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		requestContext := makeRequestContext() | ||||
| 		LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(profiles.GetProfileContinuation, auth.GoLoginContinuation)(template.TemplateContinuation("profile.html", true), template.TemplateContinuation("profile.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) | ||||
| 	}) | ||||
| 
 | ||||
| 	mux.HandleFunc("POST /profile", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		requestContext := makeRequestContext() | ||||
| 		LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(profiles.UpdateProfileContinuation(uploadAdapter, profiles.MaxAvatarSize, profiles.AvatarPath), auth.GoLoginContinuation)(profiles.GetProfileContinuation, FailurePassingContinuation)(template.TemplateContinuation("profile.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) | ||||
| 	}) | ||||
| 
 | ||||
| 	mux.HandleFunc("GET /dns", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		requestContext := makeRequestContext() | ||||
| 		LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(dns.ListDNSRecordsContinuation, auth.GoLoginContinuation)(template.TemplateContinuation("dns.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) | ||||
| 	}) | ||||
| 
 | ||||
| 	const MAX_USER_RECORDS = 100 | ||||
| 	var USER_OWNED_INTERNAL_FMT_DOMAINS = []string{"%s", "%s.endpoints"} | ||||
| 	mux.HandleFunc("POST /dns", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		requestContext := makeRequestContext() | ||||
| 		LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(dns.ListDNSRecordsContinuation, auth.GoLoginContinuation)(dns.CreateDNSRecordContinuation(cloudflareAdapter, MAX_USER_RECORDS, USER_OWNED_INTERNAL_FMT_DOMAINS), FailurePassingContinuation)(dns.ListDNSRecordsContinuation, dns.ListDNSRecordsContinuation)(template.TemplateContinuation("dns.html", true), template.TemplateContinuation("dns.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) | ||||
| 		LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(dns.ListDNSRecordsContinuation, auth.GoLoginContinuation)(dns.CreateDNSRecordContinuation(cloudflareAdapter, dns.MaxUserRecords, dns.UserOwnedInternalFmtDomains), FailurePassingContinuation)(dns.ListDNSRecordsContinuation, dns.ListDNSRecordsContinuation)(template.TemplateContinuation("dns.html", true), template.TemplateContinuation("dns.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) | ||||
| 	}) | ||||
| 
 | ||||
| 	mux.HandleFunc("POST /dns/delete", func(w http.ResponseWriter, r *http.Request) { | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ type Arguments struct { | |||
| 	DatabasePath string | ||||
| 	TemplatePath string | ||||
| 	StaticPath   string | ||||
| 	UploadPath   string | ||||
| 
 | ||||
| 	Migrate   bool | ||||
| 	Scheduler bool | ||||
|  | @ -35,6 +36,13 @@ type Arguments struct { | |||
| 
 | ||||
| func GetArgs() (*Arguments, error) { | ||||
| 	databasePath := flag.String("database-path", "./hatecomputers.db", "Path to the SQLite database") | ||||
| 
 | ||||
| 	uploadPath := flag.String("upload-path", "./uploads", "Path to the uploads directory") | ||||
| 	uploadPathValue := *uploadPath | ||||
| 	if uploadPathValue[len(uploadPathValue)-1] != '/' { | ||||
| 		uploadPathValue += "/" | ||||
| 	} | ||||
| 
 | ||||
| 	templatePath := flag.String("template-path", "./templates", "Path to the template directory") | ||||
| 	staticPath := flag.String("static-path", "./static", "Path to the static directory") | ||||
| 	dnsResolvers := flag.String("dns-resolvers", "1.1.1.1:53,1.0.0.1:53", "Comma-separated list of DNS resolvers") | ||||
|  | @ -96,6 +104,7 @@ func GetArgs() (*Arguments, error) { | |||
| 	arguments := &Arguments{ | ||||
| 		DatabasePath:    *databasePath, | ||||
| 		TemplatePath:    *templatePath, | ||||
| 		UploadPath:      uploadPathValue, | ||||
| 		StaticPath:      *staticPath, | ||||
| 		CloudflareToken: cloudflareToken, | ||||
| 		CloudflareZone:  cloudflareZone, | ||||
|  |  | |||
|  | @ -127,6 +127,39 @@ func MigrateGuestBook(dbConn *sql.DB) (*sql.DB, error) { | |||
| 	return dbConn, nil | ||||
| } | ||||
| 
 | ||||
| func MigrateProfiles(dbConn *sql.DB) (*sql.DB, error) { | ||||
| 	log.Println("migrating profiles columns") | ||||
| 
 | ||||
| 	userColumns := map[string]bool{} | ||||
| 	row, err := dbConn.Query(`PRAGMA table_info(users);`) | ||||
| 	if err != nil { | ||||
| 		return dbConn, err | ||||
| 	} | ||||
| 	defer row.Close() | ||||
| 
 | ||||
| 	for row.Next() { | ||||
| 		var columnName string | ||||
| 		row.Scan(nil, &columnName, nil, nil, nil, nil) | ||||
| 		userColumns[columnName] = true | ||||
| 	} | ||||
| 
 | ||||
| 	columns := map[string]string{} | ||||
| 	columns["bio"] = "a computer hater" | ||||
| 	columns["location"] = "earth" | ||||
| 	columns["website"] = "https://hatecomputers.club" | ||||
| 	columns["avatar"] = "/files/avatars/default.png" | ||||
| 
 | ||||
| 	for column, defaultValue := range columns { | ||||
| 		if userColumns[column] { | ||||
| 			continue | ||||
| 		} | ||||
| 		log.Println("migrating column", column) | ||||
| 		_, err = dbConn.Exec(`ALTER TABLE users ADD COLUMN ` + column + ` TEXT NOT NULL DEFAULT '` + defaultValue + `';`) | ||||
| 	} | ||||
| 
 | ||||
| 	return dbConn, nil | ||||
| } | ||||
| 
 | ||||
| func Migrate(dbConn *sql.DB) (*sql.DB, error) { | ||||
| 	log.Println("migrating database") | ||||
| 
 | ||||
|  | @ -137,6 +170,7 @@ func Migrate(dbConn *sql.DB) (*sql.DB, error) { | |||
| 		MigrateDomainOwners, | ||||
| 		MigrateDNSRecords, | ||||
| 		MigrateGuestBook, | ||||
| 		MigrateProfiles, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, migration := range migrations { | ||||
|  |  | |||
|  | @ -24,6 +24,10 @@ type User struct { | |||
| 	Mail        string    `json:"email"` | ||||
| 	Username    string    `json:"preferred_username"` | ||||
| 	DisplayName string    `json:"name"` | ||||
| 	Bio         string    `json:"bio"` | ||||
| 	Location    string    `json:"location"` | ||||
| 	Website     string    `json:"website"` | ||||
| 	Avatar      string    `json:"avatar"` | ||||
| 	CreatedAt   time.Time `json:"created_at"` | ||||
| } | ||||
| 
 | ||||
|  | @ -36,10 +40,10 @@ type UserSession struct { | |||
| func GetUser(dbConn *sql.DB, id string) (*User, error) { | ||||
| 	log.Println("getting user", id) | ||||
| 
 | ||||
| 	row := dbConn.QueryRow(`SELECT id, mail, username, display_name, created_at FROM users WHERE id = ?;`, id) | ||||
| 	row := dbConn.QueryRow(`SELECT id, mail, username, display_name, bio, location, website, avatar, created_at FROM users WHERE id = ?;`, id) | ||||
| 
 | ||||
| 	var user User | ||||
| 	err := row.Scan(&user.ID, &user.Mail, &user.Username, &user.DisplayName, &user.CreatedAt) | ||||
| 	err := row.Scan(&user.ID, &user.Mail, &user.Username, &user.DisplayName, &user.Bio, &user.Location, &user.Website, &user.Avatar, &user.CreatedAt) | ||||
| 	if err != nil { | ||||
| 		log.Println(err) | ||||
| 		return nil, err | ||||
|  | @ -48,7 +52,32 @@ func GetUser(dbConn *sql.DB, id string) (*User, error) { | |||
| 	return &user, nil | ||||
| } | ||||
| 
 | ||||
| func FindOrSaveUser(dbConn *sql.DB, user *User) (*User, error) { | ||||
| 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;`) | ||||
| 	if err != nil { | ||||
| 		log.Println(err) | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer rows.Close() | ||||
| 
 | ||||
| 	var users []*User | ||||
| 	for rows.Next() { | ||||
| 		var user User | ||||
| 		err := rows.Scan(&user.ID, &user.Mail, &user.Username, &user.DisplayName, &user.Bio, &user.Location, &user.Website, &user.Avatar, &user.CreatedAt) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			return nil, err | ||||
| 		} | ||||
| 
 | ||||
| 		users = append(users, &user) | ||||
| 	} | ||||
| 
 | ||||
| 	return users, nil | ||||
| } | ||||
| 
 | ||||
| func FindOrSaveBaseUser(dbConn *sql.DB, user *User) (*User, error) { | ||||
| 	log.Println("finding or saving user", user.ID) | ||||
| 
 | ||||
| 	_, err := dbConn.Exec(`INSERT INTO users (id, mail, username, display_name) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET mail = excluded.mail, username = excluded.username, display_name = excluded.display_name;`, user.ID, user.Mail, user.Username, user.DisplayName) | ||||
|  | @ -59,6 +88,17 @@ func FindOrSaveUser(dbConn *sql.DB, user *User) (*User, error) { | |||
| 	return user, nil | ||||
| } | ||||
| 
 | ||||
| func SaveUser(dbConn *sql.DB, user *User) (*User, error) { | ||||
| 	log.Println("saving user", user.ID) | ||||
| 
 | ||||
| 	_, err := dbConn.Exec(`INSERT 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 { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return user, nil | ||||
| } | ||||
| 
 | ||||
| func MakeUserSessionFor(dbConn *sql.DB, user *User) (*UserSession, error) { | ||||
| 	log.Println("making session for user", user.ID) | ||||
| 
 | ||||
|  |  | |||
|  | @ -15,5 +15,6 @@ services: | |||
|       - ./db:/app/db | ||||
|       - ./templates:/app/templates | ||||
|       - ./static:/app/static | ||||
|       - ./uploads:/app/uploads | ||||
|     ports: | ||||
|       - "127.0.0.1:4455:8080" | ||||
|  |  | |||
|  | @ -37,7 +37,9 @@ | |||
| 	<span> | </span> | ||||
| 	<a href="/keys">api keys.</a> | ||||
| 	<span> | </span> | ||||
| 	<a href="/logout">logout, {{ .User.DisplayName }}.</a> | ||||
| 	<a href="/profile">{{ .User.DisplayName }}.</a> | ||||
| 	<span> | </span> | ||||
| 	<a href="/logout">logout.</a> | ||||
| 	 | ||||
| 	{{ else }} | ||||
| 	<a href="/login">login.</a> | ||||
|  |  | |||
|  | @ -1,3 +1,7 @@ | |||
| {{ define "content" }} | ||||
|   <p class="blinky">under construction!</p> | ||||
|   <h2 class="blinky">hello there!</h2> | ||||
|   <p>current peeps:</p> | ||||
|   {{ range $user := .Users }} | ||||
|     <p>{{ $user.Username }}</p> | ||||
|   {{ end }} | ||||
| {{ end }} | ||||
|  |  | |||
|  | @ -0,0 +1,29 @@ | |||
| {{ define "content" }} | ||||
| 
 | ||||
| <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"> | ||||
| 
 | ||||
|   <label for="type">location.</label> | ||||
|   <input type="text" name="location" value="{{ .Profile.Location }}"> | ||||
| 
 | ||||
|   <label for="type">website.</label> | ||||
|   <input type="text" name="website" value="{{ .Profile.Website }}"> | ||||
| 
 | ||||
|   <label for="type">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.
										
									
								
							| After Width: | Height: | Size: 28 KiB | 
		Loading…
	
		Reference in New Issue