diff --git a/assets/style.css b/assets/style.css index c73b84f..a889494 100644 --- a/assets/style.css +++ b/assets/style.css @@ -86,4 +86,8 @@ dd { .radio { display: inline-block } .field { margin-bottom: 1rem; max-width: 500px; } .field > label { margin-bottom: .25rem; } -.field > * { display: block; width: 100%; box-sizing: border-box; } \ No newline at end of file +.field > * { display: block; width: 100%; box-sizing: border-box; } + +.info { + margin-bottom: 1em; +} \ No newline at end of file diff --git a/config/cfg.go b/config/cfg.go index dfcf43f..59082f4 100644 --- a/config/cfg.go +++ b/config/cfg.go @@ -4,12 +4,16 @@ import "os" type ( Config struct { - DatabaseURL string - SessionKey string - Env string - CertFile string - KeyFile string - AssetsDir string + DatabaseURL string + SessionKey string + Env string + CertFile string + KeyFile string + AssetsDir string + EmailUsername string + EmailPassword string + EmailHost string + EmailHostAddr string } ) diff --git a/model/user.go b/model/user.go index 99e1d50..3105ef5 100644 --- a/model/user.go +++ b/model/user.go @@ -8,15 +8,17 @@ import ( ) type User struct { - Name string - Password string - Hash []byte - Homepage string - About string - Style string - Picture string - Email string - CreatedAt time.Time + Name string + Password string + Hash []byte + Homepage string + About string + Picture string + Email string + SignupEmail string + SignupMsg string + Active bool + CreatedAt time.Time } func (u User) Validate() error { diff --git a/storage/migration.go b/storage/migration.go index 5028cd1..6c6375b 100644 --- a/storage/migration.go +++ b/storage/migration.go @@ -7,7 +7,7 @@ import ( "strconv" ) -const schemaVersion = 9 +const schemaVersion = 11 func Migrate(db *sql.DB) { var currentVersion int diff --git a/storage/sql.go b/storage/sql.go index 5528edd..726054a 100644 --- a/storage/sql.go +++ b/storage/sql.go @@ -25,6 +25,15 @@ create table statuses created_at timestamp with time zone DEFAULT now() ); `, + "schema_version_10": `alter table users + add column signup_email TEXT not null DEFAULT '', + add column signup_msg TEXT not null DEFAULT '', + add column active bool default false; + +-- All existing users are active +update users set active=true;`, + "schema_version_11": `ALTER TABLE statuses + ALTER COLUMN face TYPE VARCHAR(2);`, "schema_version_2": `alter table users add column status_id int references statuses(id); `, diff --git a/storage/sql/schema_version_10.sql b/storage/sql/schema_version_10.sql new file mode 100644 index 0000000..d4d293d --- /dev/null +++ b/storage/sql/schema_version_10.sql @@ -0,0 +1,7 @@ +alter table users + add column signup_email TEXT not null DEFAULT '', + add column signup_msg TEXT not null DEFAULT '', + add column active bool default false; + +-- All existing users are active +update users set active=true; \ No newline at end of file diff --git a/storage/sql/schema_version_11.sql b/storage/sql/schema_version_11.sql new file mode 100644 index 0000000..b4a64cf --- /dev/null +++ b/storage/sql/schema_version_11.sql @@ -0,0 +1,2 @@ +ALTER TABLE statuses + ALTER COLUMN face TYPE VARCHAR(2); \ No newline at end of file diff --git a/storage/status.go b/storage/status.go index f9c60b6..5f7dfee 100644 --- a/storage/status.go +++ b/storage/status.go @@ -94,6 +94,24 @@ func (s *Storage) StatusByUsername(user string, perPage int, page int64) ([]mode return statuses, false, err } +func (s *Storage) StatusFeed() ([]model.Status, error) { + rows, err := s.db.Query(statusQueryBuilder{ + limit: strconv.Itoa(20), + }.build()) + if err != nil { + return nil, err + } + var statuses []model.Status + for rows.Next() { + post, err := s.populateStatus(rows) + if err != nil { + return nil, err + } + statuses = append(statuses, post) + } + return statuses, err +} + func (s *Storage) Statuses(page int64, perPage int) ([]model.Status, bool, error) { rows, err := s.db.Query(statusQueryBuilder{ limit: strconv.Itoa(perPage + 1), diff --git a/storage/user.go b/storage/user.go index 7db079b..e1b29f2 100644 --- a/storage/user.go +++ b/storage/user.go @@ -1,13 +1,14 @@ package storage import ( + "errors" "status/model" ) -const queryFindName = `SELECT name, hash, created_at, homepage, about, picture, email FROM users WHERE name=lower($1);` +const queryFindName = `SELECT name, hash, created_at, homepage, about, picture, email, active, signup_email FROM users WHERE name=lower($1);` func (s *Storage) queryUser(q string, params ...interface{}) (user model.User, err error) { - err = s.db.QueryRow(q, params...).Scan(&user.Name, &user.Hash, &user.CreatedAt, &user.Homepage, &user.About, &user.Picture, &user.Email) + err = s.db.QueryRow(q, params...).Scan(&user.Name, &user.Hash, &user.CreatedAt, &user.Homepage, &user.About, &user.Picture, &user.Email, &user.Active, &user.SignupEmail) return } @@ -17,7 +18,10 @@ func (s *Storage) VerifyUser(user model.User) (model.User, error) { return u, err } if err := user.CompareHashToPassword(u.Hash); err != nil { - return u, err + return u, errors.New("incorrect password") + } + if !u.Active { + return u, errors.New("user not active") } return u, nil } @@ -37,12 +41,12 @@ func (s *Storage) CreateUser(user model.User) error { if err != nil { return err } - insertUser := `INSERT INTO users (name, hash) VALUES (lower($1), $2)` + insertUser := `INSERT INTO users (name, hash, email, signup_email, signup_msg) VALUES (lower($1), $2, $3, $4, $5)` statement, err := s.db.Prepare(insertUser) if err != nil { return err } - _, err = statement.Exec(user.Name, hash) + _, err = statement.Exec(user.Name, hash, user.Email, user.SignupEmail, user.SignupMsg) return err } @@ -63,13 +67,34 @@ func (s *Storage) Users() ([]string, error) { return users, nil } -func (s *Storage) DeleteUser(username string) error { - stmt, err := s.db.Prepare(`DELETE from status WHERE author = $1;`) +func (s *Storage) InactiveUsers() ([]model.User, error) { + rows, err := s.db.Query("select name, signup_email, signup_msg from users where active is false") + if err != nil { + return nil, err + } + var users []model.User + for rows.Next() { + var user model.User + err := rows.Scan(&user.Name, &user.SignupEmail, &user.SignupMsg) + if err != nil { + return users, err + } + users = append(users, user) + } + return users, nil +} + +func (s *Storage) ActivateUser(name string) error { + stmt, err := s.db.Prepare(`UPDATE users SET active = true WHERE name = $1;`) if err != nil { return err } - _, err = stmt.Exec(username) - stmt, err = s.db.Prepare(`DELETE from users WHERE name = $1;`) + _, err = stmt.Exec(name) + return err +} + +func (s *Storage) DeleteUser(username string) error { + stmt, err := s.db.Prepare(`DELETE from users WHERE name = $1;`) if err != nil { return err } diff --git a/web/handler/admin_show.go b/web/handler/admin_show.go new file mode 100644 index 0000000..9a6655e --- /dev/null +++ b/web/handler/admin_show.go @@ -0,0 +1,75 @@ +package handler + +import ( + "errors" + "fmt" + "net/http" +) + +func (h *Handler) showAdminView(w http.ResponseWriter, r *http.Request) { + protectClickJacking(w) + username, err := h.getUser(r) + if err != nil { + unauthorized(w) + return + } + if username != "m15o" { + unauthorized(w) + return + } + users, err := h.storage.InactiveUsers() + if err != nil { + serverError(w, err) + return + } + h.renderLayout(w, "admin", map[string]interface{}{ + "inactive": users, + }, username) +} + +func (h *Handler) activateUser(w http.ResponseWriter, r *http.Request) { + protectClickJacking(w) + username, err := h.getUser(r) + if err != nil { + unauthorized(w) + return + } + if username != "m15o" { + unauthorized(w) + return + } + name := r.URL.Query().Get("name") + if err := h.storage.ActivateUser(name); err != nil { + serverError(w, err) + return + } + http.Redirect(w, r, fmt.Sprintf("/admin"), http.StatusFound) +} + +func (h *Handler) deleteUser(w http.ResponseWriter, r *http.Request) { + protectClickJacking(w) + username, err := h.getUser(r) + if err != nil { + unauthorized(w) + return + } + if username != "m15o" { + unauthorized(w) + return + } + name := r.URL.Query().Get("name") + user, err := h.storage.UserByName(name) + if err != nil { + serverError(w, err) + return + } + if user.Active { + serverError(w, errors.New("user is active")) + return + } + if err := h.storage.DeleteUser(name); err != nil { + serverError(w, err) + return + } + http.Redirect(w, r, fmt.Sprintf("/admin"), http.StatusFound) +} diff --git a/web/handler/feed_show.go b/web/handler/feed_show.go new file mode 100644 index 0000000..776d10f --- /dev/null +++ b/web/handler/feed_show.go @@ -0,0 +1,47 @@ +package handler + +import ( + "fmt" + "github.com/gorilla/feeds" + "net/http" + "time" +) + +func (h *Handler) showFeedView(w http.ResponseWriter, r *http.Request) { + now := time.Now() + feed := &feeds.Feed{ + Title: "status.cafe", + Link: &feeds.Link{Href: "https://status.cafe/"}, + Author: &feeds.Author{Name: "status.cafe"}, + Created: now, + } + + statuses, err := h.storage.StatusFeed() + if err != nil { + serverError(w, err) + return + } + + for _, status := range statuses { + if err != nil { + serverError(w, err) + return + } + feed.Items = append(feed.Items, &feeds.Item{ + Title: fmt.Sprintf("%s %s %s", status.User, status.Face, truncate(status.Content, 50)), + Link: &feeds.Link{Href: fmt.Sprintf("https://status.cafe/users/%s/%d", status.User, status.Id)}, + Author: &feeds.Author{Name: status.User}, + Content: status.Content, + Created: status.CreatedAt, + }) + } + atom, err := feed.ToAtom() + if err != nil { + serverError(w, err) + return + } + + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Content-Type", "application/atom+xml") + w.Write([]byte(atom)) +} diff --git a/web/handler/form/register.go b/web/handler/form/register.go index 3e86421..82e7d94 100644 --- a/web/handler/form/register.go +++ b/web/handler/form/register.go @@ -7,11 +7,14 @@ import ( ) type RegisterForm struct { - Username string - Password string - Confirm string - Key string - Error string + Username string + Password string + Email string + ShowEmail bool + Answer string + Confirm string + Key string + Error string } func (f *RegisterForm) Validate() error { @@ -33,9 +36,12 @@ func (f *RegisterForm) Validate() error { func NewRegisterForm(r *http.Request) *RegisterForm { return &RegisterForm{ - Username: r.FormValue("name"), - Password: r.FormValue("password"), - Confirm: r.FormValue("password-confirm"), - Key: r.FormValue("key"), + Username: r.FormValue("name"), + Password: r.FormValue("password"), + Confirm: r.FormValue("password-confirm"), + Email: r.FormValue("email"), + ShowEmail: r.FormValue("show-email") == "1", + Answer: r.FormValue("answer"), + Key: r.FormValue("key"), } } diff --git a/web/handler/handler.go b/web/handler/handler.go index fed11ca..874a794 100644 --- a/web/handler/handler.go +++ b/web/handler/handler.go @@ -52,11 +52,17 @@ func New(cfg *config.Config, sess *session.Session, data *storage.Storage) (http sess: sess, } h.initTpl() + router.HandleFunc("/", h.showIndexView).Methods(http.MethodGet) router.HandleFunc("/login", h.showLoginView).Methods(http.MethodGet) router.HandleFunc("/check-login", h.checkLogin).Methods(http.MethodPost) router.HandleFunc("/register", h.handleRegister) router.HandleFunc("/logout", h.logout).Methods(http.MethodGet) + router.HandleFunc("/feed.atom", h.showFeedView).Methods(http.MethodGet) + + router.HandleFunc("/admin", h.showAdminView).Methods(http.MethodGet) + router.HandleFunc("/activate-user", h.activateUser).Methods(http.MethodGet) + router.HandleFunc("/delete-user", h.deleteUser).Methods(http.MethodGet) router.HandleFunc("/settings", h.showSettingsView).Methods(http.MethodGet) router.HandleFunc("/settings-update", h.updateSettings).Methods(http.MethodPost) diff --git a/web/handler/html.go b/web/handler/html.go index fe214ab..3f34e40 100644 --- a/web/handler/html.go +++ b/web/handler/html.go @@ -3,6 +3,17 @@ package handler var TplMap = map[string]string{ + "admin": `{{ define "content" }} +
+

Admin

+ {{ range .inactive }} +
+
{{ .Name }} ({{ .SignupEmail }}) Activate | Delete
+

{{ .SignupMsg }}

+
+ {{ end }} +
+{{ end }}`, "confirm_remove_status": `{{ define "content" }}
Are you sure you you want to delete the following status? @@ -149,6 +160,7 @@ var TplMap = map[string]string{
  • status updater bookmarklet
  • status widget for your homepage
  • +

    Subscribe via Atom

    {{ else }}

    Welcome!

    status.cafe is a place to share your current status.

    @@ -246,26 +258,54 @@ var TplMap = map[string]string{ {{ end }}`, "register": `{{ define "content" }}
    -

    Register

    - {{ if .form.Error }} -

    {{ .form.Error }}

    - {{ end }} -
    -
    - - +
    +
    +

    Register

    +
    +

    Registrations are manually approved to prevent spam and keep this little cafe a cool place to hang out.

    +

    You should receive a confirmation within a few hours at the email you have provided. Make sure to enter a valid address!

    +
    -
    - - +
    + {{ if .form.Error }} +

    {{ .form.Error }}

    + {{ end }} + +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    -
    - - -
    -
    - - +
    +
    +{{ end }}`, + "register-success": `{{ define "content" }} +
    +

    Thank you!

    +

    Thanks for registering, {{ .name }}!

    +

    You should receive a confirmation email on {{ .email }} as soon as your account is activated.

    {{ end }}`, "settings": `{{ define "content" }} diff --git a/web/handler/html/admin.html b/web/handler/html/admin.html new file mode 100644 index 0000000..2ecb2b9 --- /dev/null +++ b/web/handler/html/admin.html @@ -0,0 +1,11 @@ +{{ define "content" }} +
    +

    Admin

    + {{ range .inactive }} +
    +
    {{ .Name }} ({{ .SignupEmail }}) Activate | Delete
    +

    {{ .SignupMsg }}

    +
    + {{ end }} +
    +{{ end }} \ No newline at end of file diff --git a/web/handler/html/index.html b/web/handler/html/index.html index 3f2bc53..bce78f9 100644 --- a/web/handler/html/index.html +++ b/web/handler/html/index.html @@ -12,6 +12,7 @@
  • status updater bookmarklet
  • status widget for your homepage
  • +

    Subscribe via Atom

    {{ else }}

    Welcome!

    status.cafe is a place to share your current status.

    diff --git a/web/handler/html/register-success.html b/web/handler/html/register-success.html new file mode 100644 index 0000000..255edec --- /dev/null +++ b/web/handler/html/register-success.html @@ -0,0 +1,7 @@ +{{ define "content" }} +
    +

    Thank you!

    +

    Thanks for registering, {{ .name }}!

    +

    You should receive a confirmation email on {{ .email }} as soon as your account is activated.

    +
    +{{ end }} \ No newline at end of file diff --git a/web/handler/html/register.html b/web/handler/html/register.html index d05fd59..8b89125 100644 --- a/web/handler/html/register.html +++ b/web/handler/html/register.html @@ -1,24 +1,45 @@ {{ define "content" }}
    -

    Register

    - {{ if .form.Error }} -

    {{ .form.Error }}

    - {{ end }} -
    -
    - - +
    +
    +

    Register

    +
    +

    Registrations are manually approved to prevent spam and keep this little cafe a cool place to hang out.

    +

    You should receive a confirmation within a few hours at the email you have provided. Make sure to enter a valid address!

    +
    -
    - - +
    + {{ if .form.Error }} +

    {{ .form.Error }}

    + {{ end }} + +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    -
    - - -
    -
    - - +
    {{ end }} \ No newline at end of file diff --git a/web/handler/login_check.go b/web/handler/login_check.go index 81f9167..9019ccd 100644 --- a/web/handler/login_check.go +++ b/web/handler/login_check.go @@ -13,7 +13,7 @@ func (h *Handler) checkLogin(w http.ResponseWriter, r *http.Request) { Password: f.Password, }) if err != nil { - f.Error = "incorrect password" + f.Error = err.Error() h.renderLayout(w, "login", map[string]interface{}{ "form": f, }, "") diff --git a/web/handler/register.go b/web/handler/register.go index 09d718a..d5ed583 100644 --- a/web/handler/register.go +++ b/web/handler/register.go @@ -93,8 +93,13 @@ func (h *Handler) register(w http.ResponseWriter, r *http.Request) { return } user := model.User{ - Name: f.Username, - Password: f.Password, + Name: f.Username, + Password: f.Password, + SignupEmail: f.Email, + SignupMsg: f.Answer, + } + if f.ShowEmail { + user.Email = f.Email } if err := user.Validate(); err != nil { showError(err) @@ -108,10 +113,9 @@ func (h *Handler) register(w http.ResponseWriter, r *http.Request) { showError(err) return } - if err := h.sess.Save(r, w, r.FormValue("name")); err != nil { - serverError(w, err) - return - } - http.Redirect(w, r, "/", http.StatusFound) + h.renderLayout(w, "register-success", map[string]interface{}{ + "name": user.Name, + "email": user.SignupEmail, + }, "") } } diff --git a/web/handler/user_show.go b/web/handler/user_show.go index a0a465d..f87fc94 100644 --- a/web/handler/user_show.go +++ b/web/handler/user_show.go @@ -82,7 +82,6 @@ func (h *Handler) showUserView(w http.ResponseWriter, r *http.Request) { "about": template.HTML(user.About), "picture": user.Picture, "email": user.Email, - "style": template.CSS(user.Style), "showMore": showMore, "page": page, "next_page": page + 1, diff --git a/web/session/session.go b/web/session/session.go index 579e661..dbf92d0 100644 --- a/web/session/session.go +++ b/web/session/session.go @@ -52,8 +52,12 @@ func (s *Session) Get(r *http.Request) (string, error) { if name == "" || !ok { return "", errors.New("error extracting session") } - if ok := s.Storage.UserExists(name); !ok { - return "", errors.New("user doesn't exit") + u, err := s.Storage.UserByName(name) + if err != nil { + return "", errors.New("error getting user") + } + if !u.Active { + return "", errors.New("user not active") } return name, nil }