Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 43 additions & 43 deletions front/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,55 +16,55 @@

Users are authenticated using TLS client certificates; see [Gemini protocol specification](https://gemini.circumlunar.space/docs/specification.html) for more details. The following pages require authentication:

* /users shows posts by followed users, sorted chronologically.
* /users/mentions is like /users but shows only posts that mention the user.
* /users/register creates a new user.
* /users/follows shows a list of followed users, ordered by activity.
* /users/me redirects the user to their outbox.
* /users/resolve looks up federated user *user@domain* or local user *user*.
* /users/dm creates a post visible to mentioned users.
* /users/whisper creates a post visible to followers.
* /users/say creates a public post.
* /users/reply replies to a post.
* /users/edit edits a post.
* /users/delete deletes a post.
* /users/share shares a post.
* /users/unshare removes a shared post.
* /users/follow sends a follow request to a user.
* /users/unfollow deletes a follow request.
* /users/outbox is equivalent to /outbox but also includes a link to /users/follow or /users/unfollow.
* /users/bio allows users to edit their bio.
* /users/name allows users to set their display name.
* /users/alias allows users to set an account alias, to allow migration of accounts to tootik.
* /users/move allows users to notify followers of account migration from tootik.

Some clients generate a certificate for / (all pages of this capsule) when /foo requests a client certificate, while others use the certificate requested by /foo only for /foo and /foo/bar. Therefore, pages that don't require authentication are also mirrored under /users:

* /users/local
* /users/hashtag
* /users/hashtags
* /users/fts
* /users/status
* /users/view
* /users/thread

This way, users who prefer not to provide a client certificate when browsing to /x can reply to public posts by using /users/x instead.

To make the transition to authenticated pages more seamless, links in the user menu at the bottom of each page point to /users/x rather than /x, if the user is authenticated.

All pages follow the [subscription convention](https://gemini.circumlunar.space/docs/companion/subscription.gmi), so users can "subscribe" to a user, a hashtag, posts by followed users or other activity. This way, tootik can act as a personal fediverse aggregator. In addition, feeds like /users have separators between days, to interrupt the endless stream of incoming content, make the content consumption more intentional and prevent doomscrolling.
* /login shows posts by followed users, sorted chronologically.
* /login/mentions is like /login but shows only posts that mention the user.
* /login/register creates a new user.
* /login/follows shows a list of followed users, ordered by activity.
* /login/me redirects the user to their outbox.
* /login/resolve looks up federated user *user@domain* or local user *user*.
* /login/dm creates a post visible to mentioned users.
* /login/whisper creates a post visible to followers.
* /login/say creates a public post.
* /login/reply replies to a post.
* /login/edit edits a post.
* /login/delete deletes a post.
* /login/share shares a post.
* /login/unshare removes a shared post.
* /login/follow sends a follow request to a user.
* /login/unfollow deletes a follow request.
* /login/outbox is equivalent to /outbox but also includes a link to /login/follow or /login/unfollow.
* /login/bio allows users to edit their bio.
* /login/name allows users to set their display name.
* /login/alias allows users to set an account alias, to allow migration of accounts to tootik.
* /login/move allows users to notify followers of account migration from tootik.

Some clients generate a certificate for / (all pages of this capsule) when /foo requests a client certificate, while others use the certificate requested by /foo only for /foo and /foo/bar. Therefore, pages that don't require authentication are also mirrored under /login:

* /login/local
* /login/hashtag
* /login/hashtags
* /login/fts
* /login/status
* /login/view
* /login/thread

This way, users who prefer not to provide a client certificate when browsing to /x can reply to public posts by using /login/x instead.

To make the transition to authenticated pages more seamless, links in the user menu at the bottom of each page point to /login/x rather than /x, if the user is authenticated.

All pages follow the [subscription convention](https://gemini.circumlunar.space/docs/companion/subscription.gmi), so users can "subscribe" to a user, a hashtag, posts by followed users or other activity. This way, tootik can act as a personal fediverse aggregator. In addition, feeds like /login have separators between days, to interrupt the endless stream of incoming content, make the content consumption more intentional and prevent doomscrolling.

## Authentication

If no client certificate is provided, all pages under /users redirect the client to /users.
If no client certificate is provided, all pages under /login redirect the client to /login.

/users asks the client to provide a certificate. Well-behaved clients should generate a certificate, re-request /users, then reuse this certificate in future requests of /users and pages under it.
/login asks the client to provide a certificate. Well-behaved clients should generate a certificate, re-request /login, then reuse this certificate in future requests of /login and pages under it.

If a certificate is provided but does not belong to any user, the client is redirected to /users/register.
If a certificate is provided but does not belong to any user, the client is redirected to /login/register.

By default, the username associated with a client certificate is the common name specified in the certificate. If invalid or already in use by another user, /users/register asks the user to provide a different username. Once the user is registered, the client is redirected back to /users.
By default, the username associated with a client certificate is the common name specified in the certificate. If invalid or already in use by another user, /login/register asks the user to provide a different username. Once the user is registered, the client is redirected back to /login.

Once the client certificate is associated with a user, all pages under /users look up the authenticated user's data using the certificate hash.
Once the client certificate is associated with a user, all pages under /login look up the authenticated user's data using the certificate hash.

## Posts

Expand All @@ -91,7 +91,7 @@ tootik has three kinds of posts:

## Post Editing

/users/edit cannot remove recipients from the post audience, only add more. If a post that mentions only `@a` is edited to mention only `@b`, both `a` and `b` will receive the updated post.
/login/edit cannot remove recipients from the post audience, only add more. If a post that mentions only `@a` is edited to mention only `@b`, both `a` and `b` will receive the updated post.

### Polls

Expand Down
4 changes: 2 additions & 2 deletions front/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (

func (h *Handler) alias(w text.Writer, r *Request, args ...string) {
if r.User == nil {
w.Redirect("/users")
w.Redirect("/login")
return
}

Expand Down Expand Up @@ -101,5 +101,5 @@ func (h *Handler) alias(w text.Writer, r *Request, args ...string) {
return
}

w.Redirect("/users/outbox/" + strings.TrimPrefix(actor.ID, "https://"))
w.Redirect("/login/outbox/" + strings.TrimPrefix(actor.ID, "https://"))
}
4 changes: 2 additions & 2 deletions front/approve.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import "github.com/dimkr/tootik/front/text"

func (h *Handler) approve(w text.Writer, r *Request, args ...string) {
if r.User == nil {
w.Redirect("/users")
w.Redirect("/login")
return
}

Expand Down Expand Up @@ -50,5 +50,5 @@ func (h *Handler) approve(w text.Writer, r *Request, args ...string) {
return
}

w.Redirect("/users/certificates")
w.Redirect("/login/certificates")
}
4 changes: 2 additions & 2 deletions front/avatar.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var supportedImageTypes = map[string]struct{}{

func (h *Handler) uploadAvatar(w text.Writer, r *Request, args ...string) {
if r.User == nil || r.Body == nil {
w.Redirectf("gemini://%s/users/oops", h.Domain)
w.Redirectf("gemini://%s/login/oops", h.Domain)
return
}

Expand Down Expand Up @@ -149,5 +149,5 @@ func (h *Handler) uploadAvatar(w text.Writer, r *Request, args ...string) {
return
}

w.Redirectf("gemini://%s/users/outbox/%s", h.Domain, strings.TrimPrefix(r.User.ID, "https://"))
w.Redirectf("gemini://%s/login/outbox/%s", h.Domain, strings.TrimPrefix(r.User.ID, "https://"))
}
4 changes: 2 additions & 2 deletions front/bio.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (

func (h *Handler) doBio(w text.Writer, r *Request, readInput func(text.Writer, *Request) (string, bool)) {
if r.User == nil {
w.Redirect("/users")
w.Redirect("/login")
return
}

Expand Down Expand Up @@ -86,7 +86,7 @@ func (h *Handler) doBio(w text.Writer, r *Request, readInput func(text.Writer, *
return
}

w.Redirect("/users/outbox/" + strings.TrimPrefix(r.User.ID, "https://"))
w.Redirect("/login/outbox/" + strings.TrimPrefix(r.User.ID, "https://"))
}

func (h *Handler) bio(w text.Writer, r *Request, args ...string) {
Expand Down
4 changes: 2 additions & 2 deletions front/bookmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (

func (h *Handler) bookmark(w text.Writer, r *Request, args ...string) {
if r.User == nil {
w.Redirect("/users")
w.Redirect("/login")
return
}

Expand Down Expand Up @@ -105,5 +105,5 @@ func (h *Handler) bookmark(w text.Writer, r *Request, args ...string) {
return
}

w.Redirectf("/users/view/" + args[1])
w.Redirectf("/login/view/" + args[1])
}
8 changes: 4 additions & 4 deletions front/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (

func (h *Handler) certificates(w text.Writer, r *Request, args ...string) {
if r.User == nil {
w.Redirect("/users")
w.Redirect("/login")
return
}

Expand Down Expand Up @@ -67,10 +67,10 @@ func (h *Handler) certificates(w text.Writer, r *Request, args ...string) {
w.Item("Expires: " + time.Unix(expires, 0).Format(time.DateOnly))

if approved == 0 {
w.Link("/users/certificates/approve/"+hash, "🟢 Approve")
w.Link("/users/certificates/revoke/"+hash, "🔴 Deny")
w.Link("/login/certificates/approve/"+hash, "🟢 Approve")
w.Link("/login/certificates/revoke/"+hash, "🔴 Deny")
} else {
w.Link("/users/certificates/revoke/"+hash, "🔴 Revoke")
w.Link("/login/certificates/revoke/"+hash, "🔴 Revoke")
}

first = false
Expand Down
2 changes: 1 addition & 1 deletion front/communities.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (h *Handler) communities(w text.Writer, r *Request, args ...string) {
if r.User == nil {
w.Linkf("/outbox/"+strings.TrimPrefix(id, "https://"), "%s %s", time.Unix(last, 0).Format(time.DateOnly), username)
} else {
w.Linkf("/users/outbox/"+strings.TrimPrefix(id, "https://"), "%s %s", time.Unix(last, 0).Format(time.DateOnly), username)
w.Linkf("/login/outbox/"+strings.TrimPrefix(id, "https://"), "%s %s", time.Unix(last, 0).Format(time.DateOnly), username)
}

empty = false
Expand Down
4 changes: 2 additions & 2 deletions front/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (

func (h *Handler) delete(w text.Writer, r *Request, args ...string) {
if r.User == nil {
w.Redirect("/users")
w.Redirect("/login")
return
}

Expand All @@ -51,5 +51,5 @@ func (h *Handler) delete(w text.Writer, r *Request, args ...string) {
return
}

w.Redirect("/users/outbox/" + strings.TrimPrefix(r.User.ID, "https://"))
w.Redirect("/login/outbox/" + strings.TrimPrefix(r.User.ID, "https://"))
}
4 changes: 2 additions & 2 deletions front/dm.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (

func (h *Handler) dm(w text.Writer, r *Request, args ...string) {
if r.User == nil {
w.Redirect("/users")
w.Redirect("/login")
return
}

Expand All @@ -37,7 +37,7 @@ func (h *Handler) dm(w text.Writer, r *Request, args ...string) {

func (h *Handler) uploadDM(w text.Writer, r *Request, args ...string) {
if r.User == nil {
w.Redirect("/users")
w.Redirect("/login")
return
}

Expand Down
2 changes: 1 addition & 1 deletion front/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (

func (h *Handler) doEdit(w text.Writer, r *Request, args []string, readInput inputFunc) {
if r.User == nil {
w.Redirect("/users")
w.Redirect("/login")
return
}

Expand Down
2 changes: 1 addition & 1 deletion front/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ var csvHeader = []string{"ID", "Type", "Inserted", "Activity"}

func (h *Handler) export(w text.Writer, r *Request, args ...string) {
if r.User == nil {
w.Redirect("/users")
w.Redirect("/login")
return
}

Expand Down
4 changes: 2 additions & 2 deletions front/follow.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (

func (h *Handler) follow(w text.Writer, r *Request, args ...string) {
if r.User == nil {
w.Redirect("/users")
w.Redirect("/login")
return
}

Expand Down Expand Up @@ -71,5 +71,5 @@ func (h *Handler) follow(w text.Writer, r *Request, args ...string) {
return
}

w.Redirectf("/users/outbox/" + args[1])
w.Redirectf("/login/outbox/" + args[1])
}
6 changes: 3 additions & 3 deletions front/follows.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (

func (h *Handler) follows(w text.Writer, r *Request, args ...string) {
if r.User == nil {
w.Redirect("/users")
w.Redirect("/login")
return
}

Expand Down Expand Up @@ -91,9 +91,9 @@ func (h *Handler) follows(w text.Writer, r *Request, args ...string) {
displayName := h.getActorDisplayName(&actor)

if last.Valid {
w.Linkf("/users/outbox/"+strings.TrimPrefix(actor.ID, "https://"), "%s %s", time.Unix(last.Int64*(60*60*24), 0).Format(time.DateOnly), displayName)
w.Linkf("/login/outbox/"+strings.TrimPrefix(actor.ID, "https://"), "%s %s", time.Unix(last.Int64*(60*60*24), 0).Format(time.DateOnly), displayName)
} else {
w.Link("/users/outbox/"+strings.TrimPrefix(actor.ID, "https://"), displayName)
w.Link("/login/outbox/"+strings.TrimPrefix(actor.ID, "https://"), displayName)
}

i++
Expand Down
8 changes: 4 additions & 4 deletions front/gemini/gemini.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,9 @@ func (gl *Listener) Handle(ctx context.Context, conn net.Conn) {
defer w.Flush()

r.User, r.Key, err = gl.getUser(ctx, tlsConn)
if err != nil && errors.Is(err, front.ErrNotRegistered) && r.URL.Path == "/users" {
if err != nil && errors.Is(err, front.ErrNotRegistered) && r.URL.Path == "/login" {
slog.Info("Redirecting new user")
w.Redirect("/users/register")
w.Redirect("/login/register")
return
} else if errors.Is(err, front.ErrNotApproved) {
w.Status(40, "Client certificate is awaiting approval")
Expand All @@ -158,10 +158,10 @@ func (gl *Listener) Handle(ctx context.Context, conn net.Conn) {
slog.Warn("Failed to get user", "error", err)
w.Error()
return
} else if err == nil && r.User == nil && r.URL.Path == "/users" {
} else if err == nil && r.User == nil && r.URL.Path == "/login" {
w.Status(60, "Client certificate required")
return
} else if r.User == nil && gl.Config.RequireRegistration && r.URL.Path != "/" && r.URL.Path != "/help" && r.URL.Path != "/users/register" {
} else if r.User == nil && gl.Config.RequireRegistration && r.URL.Path != "/" && r.URL.Path != "/help" && r.URL.Path != "/login/register" {
w.Status(40, "Must register first")
return
}
Expand Down
Loading
Loading