profiles #7
|
@ -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