Compare commits

...

4 commits

Author SHA1 Message Date
Vivian Lim ⭐ 21dc52abcd
Merge 2cd5abfdcf into 3f7dc10449 2024-10-02 14:59:05 +01:00
tobi 3f7dc10449
[docs] Update smtp docs to mention starttls + port 587 (#3378)
* [docs] Update smtp docs to mention starttls + port 587

* remove misleading ssl bit

* further tweaks
2024-10-02 10:59:29 +00:00
kim c17abea921
update go-structr to v0.8.11 (#3380) 2024-10-02 10:58:20 +00:00
vivlim 2cd5abfdcf Add login button to index page which reiterates info about clients 2024-09-29 21:06:13 -07:00
21 changed files with 569 additions and 108 deletions

View file

@ -8,6 +8,18 @@ In order to make GoToSocial email sending work, you need an smtp-compatible mail
To validate your configuration, you can use the "Administration -> Actions -> Email" section of the settings panel to send a test email.
!!! warning
Pending an smtp library update, currently only email providers that work with STARTTLS will work with GoToSocial. STARTTLS is generally available over **port 587**.
For more info, see:
- [STARTTLS vs SSL vs TLS](https://mailtrap.io/blog/starttls-ssl-tls/)
- [Understanding Ports](https://www.mailgun.com/blog/email/which-smtp-port-understanding-ports-25-465-587/)
- [Port 587](https://www.mailgun.com/blog/deliverability/smtp-port-587/)
!!! info
For safety reasons, the smtp library used by GoToSocial will refuse to send authentication credentials over an unencrypted connection, unless the mail provider is running on localhost.
## Settings
The configuration options for smtp are as follows:
@ -26,6 +38,7 @@ The configuration options for smtp are as follows:
smtp-host: ""
# Int. Port to use to connect to the smtp server.
# In the majority of cases, you should use port 587.
# Examples: []
# Default: 0
smtp-port: 0
@ -63,27 +76,16 @@ smtp-disclose-recipients: false
Note that if you don't set `Host`, then email sending via smtp will be disabled, and the other settings will be ignored. GoToSocial will still log (at trace level) emails that *would* have been sent if smtp was enabled.
## Behavior
### SSL
GoToSocial requires your smtp server to present valid SSL certificates. Most of the big services like Mailgun do this anyway, but if you're running your own mail server without SSL for some reason, and you're trying to connect GoToSocial to it, it will not work.
The exception to this requirement is if you're running your mail server (or bridge to a mail server) on `localhost`, in which case SSL certs are not required.
### When are emails sent?
## When are emails sent?
Currently, emails are sent:
- To the provided email address of a new user to request email confirmation when a new account is created via the API.
- To the provided email address of a new user to request email confirmation when a new account is created via the sign up page or API.
- To instance admins when a new account is created in this way.
- To all active instance moderators + admins when a new moderation report is received. By default, recipients are Bcc'd, but you can change this behavior with the setting `smtp-disclose-recipients`.
- To the creator of a report (on this instance) when the report is closed by a moderator.
### Can I test if my SMTP configuration is correct?
Yes, you can use the API to send a test email to yourself. Check the API documentation for the `/api/v1/admin/email/test` endpoint.
### HTML versus Plaintext
## HTML versus Plaintext
Emails are sent in plaintext by default. At this point, there is no option to send emails in html, but this is something that might be added later if there's enough demand for it.

View file

@ -817,6 +817,7 @@ oidc-admin-groups: []
smtp-host: ""
# Int. Port to use to connect to the smtp server.
# In the majority of cases, you should use port 587.
# Examples: []
# Default: 0
smtp-port: 0

2
go.mod
View file

@ -22,7 +22,7 @@ require (
codeberg.org/gruf/go-runners v1.6.3
codeberg.org/gruf/go-sched v1.2.4
codeberg.org/gruf/go-storage v0.2.0
codeberg.org/gruf/go-structr v0.8.10
codeberg.org/gruf/go-structr v0.8.11
codeberg.org/superseriousbusiness/exif-terminator v0.9.0
github.com/DmitriyVTitov/size v1.5.0
github.com/KimMachineGun/automemlimit v0.6.1

4
go.sum
View file

@ -72,8 +72,8 @@ codeberg.org/gruf/go-sched v1.2.4 h1:ddBB9o0D/2oU8NbQ0ldN5aWxogpXPRBATWi58+p++Hw
codeberg.org/gruf/go-sched v1.2.4/go.mod h1:wad6l+OcYGWMA2TzNLMmLObsrbBDxdJfEy5WvTgBjNk=
codeberg.org/gruf/go-storage v0.2.0 h1:mKj3Lx6AavEkuXXtxqPhdq+akW9YwrnP16yQBF7K5ZI=
codeberg.org/gruf/go-storage v0.2.0/go.mod h1:o3GzMDE5QNUaRnm/daUzFqvuAaC4utlgXDXYO79sWKU=
codeberg.org/gruf/go-structr v0.8.10 h1:uSapW97/StRnYEhCtycaM0isCsEMYC+tx/knYr6SiVo=
codeberg.org/gruf/go-structr v0.8.10/go.mod h1:zkoXVrAnKosh8VFAsbP/Hhs8FmLBjbVVy5w/Ngm8ApM=
codeberg.org/gruf/go-structr v0.8.11 h1:I3cQCHpK3fQSXWaaUfksAJRN4+efULiuF11Oi/m8c+o=
codeberg.org/gruf/go-structr v0.8.11/go.mod h1:zkoXVrAnKosh8VFAsbP/Hhs8FmLBjbVVy5w/Ngm8ApM=
codeberg.org/superseriousbusiness/exif-terminator v0.9.0 h1:/EfyGI6HIrbkhFwgXGSjZ9o1kr/+k8v4mKdfXTH02Go=
codeberg.org/superseriousbusiness/exif-terminator v0.9.0/go.mod h1:gCWKduudUWFzsnixoMzu0FYVdxHWG+AbXnZ50DqxsUE=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=

View file

@ -60,7 +60,7 @@ func (m *Module) indexHandler(c *gin.Context) {
Instance: instance,
OGMeta: apiutil.OGBase(instance),
Stylesheets: []string{cssAbout, cssIndex},
Extra: map[string]any{"showStrap": true},
Extra: map[string]any{"showStrap": true, "showLoginButton": true},
}
apiutil.TemplateWebPage(c, page)

63
internal/web/login.go Normal file
View file

@ -0,0 +1,63 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package web
import (
"context"
"github.com/gin-gonic/gin"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
const (
loginPath = "/login"
)
func (m *Module) loginGETHandler(c *gin.Context) {
instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
if errWithCode != nil {
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
// Return instance we already got from the db,
// don't try to fetch it again when erroring.
instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
return instance, nil
}
// We only serve text/html at this endpoint.
if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil {
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet)
return
}
page := apiutil.WebPage{
Template: "login.tmpl",
Instance: instance,
OGMeta: apiutil.OGBase(instance),
Stylesheets: []string{cssLogin},
Extra: map[string]any{
"showStrap": false,
},
}
apiutil.TemplateWebPage(c, page)
}

View file

@ -61,6 +61,7 @@
cssFA = assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css"
cssAbout = distPathPrefix + "/about.css"
cssIndex = distPathPrefix + "/index.css"
cssLogin = distPathPrefix + "/login.css"
cssStatus = distPathPrefix + "/status.css"
cssThread = distPathPrefix + "/thread.css"
cssProfile = distPathPrefix + "/profile.css"
@ -119,6 +120,7 @@ func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) {
r.AttachHandler(http.MethodPost, confirmEmailPath, m.confirmEmailPOSTHandler)
r.AttachHandler(http.MethodGet, robotsPath, m.robotsGETHandler)
r.AttachHandler(http.MethodGet, aboutPath, m.aboutGETHandler)
r.AttachHandler(http.MethodGet, loginPath, m.loginGETHandler)
r.AttachHandler(http.MethodGet, domainBlockListPath, m.domainBlockListGETHandler)
r.AttachHandler(http.MethodGet, tagsPath, m.tagGETHandler)
r.AttachHandler(http.MethodGet, signupPath, m.signupGETHandler)

View file

@ -119,9 +119,9 @@ func (c *Cache[T]) Init(config CacheConfig[T]) {
// Index selects index with given name from cache, else panics.
func (c *Cache[T]) Index(name string) *Index {
for i := range c.indices {
if c.indices[i].name == name {
return &c.indices[i]
for i, idx := range c.indices {
if idx.name == name {
return &(c.indices[i])
}
}
panic("unknown index: " + name)
@ -337,13 +337,16 @@ func (c *Cache[T]) Load(index *Index, keys []Key, load func([]Key) ([]T, error))
panic("not initialized")
}
for i := 0; i < len(keys); {
// Iterate keys and catch uncached.
toLoad := make([]Key, 0, len(keys))
for _, key := range keys {
// Value length before
// any below appends.
before := len(values)
// Concatenate all *values* from cached items.
index.get(keys[i].key, func(item *indexed_item) {
index.get(key.key, func(item *indexed_item) {
if value, ok := item.data.(T); ok {
// Append value COPY.
value = c.copy(value)
@ -358,30 +361,22 @@ func (c *Cache[T]) Load(index *Index, keys []Key, load func([]Key) ([]T, error))
// Only if values changed did
// we actually find anything.
if len(values) != before {
// We found values at key,
// drop key from the slice.
copy(keys[i:], keys[i+1:])
keys = keys[:len(keys)-1]
continue
if len(values) == before {
toLoad = append(toLoad, key)
}
// Iter
i++
}
// Done with
// the lock.
unlock()
if len(keys) == 0 {
if len(toLoad) == 0 {
// We loaded everything!
return values, nil
}
// Load uncached values.
uncached, err := load(keys)
// Load uncached key values.
uncached, err := load(toLoad)
if err != nil {
return nil, err
}
@ -515,8 +510,8 @@ func (c *Cache[T]) Trim(perc float64) {
}
// Compact index data stores.
for i := range c.indices {
c.indices[i].data.Compact()
for _, idx := range c.indices {
(&idx).data.Compact()
}
// Done with lock.
@ -536,17 +531,17 @@ func (c *Cache[T]) Len() int {
// Debug returns debug stats about cache.
func (c *Cache[T]) Debug() map[string]any {
m := make(map[string]any)
m := make(map[string]any, 2)
c.mutex.Lock()
m["lru"] = c.lru.len
indices := make(map[string]any)
indices := make(map[string]any, len(c.indices))
m["indices"] = indices
for i := range c.indices {
for _, idx := range c.indices {
var n uint64
for _, l := range c.indices[i].data.m {
for _, l := range idx.data.m {
n += uint64(l.len)
}
indices[c.indices[i].name] = n
indices[idx.name] = n
}
c.mutex.Unlock()
return m
@ -588,7 +583,7 @@ func (c *Cache[T]) store_value(index *Index, key string, value T) {
for i := range c.indices {
// Get current index ptr.
idx := &(c.indices[i])
idx := (&c.indices[i])
if idx == index {
// Already stored under

View file

@ -197,8 +197,13 @@ func (i *Index) get(key string, hook func(*indexed_item)) {
return
}
// Iterate all entries in list.
l.rangefn(func(elem *list_elem) {
// Iterate the list.
for elem := l.head; //
elem != nil; //
{
// Get next before
// any modification.
next := elem.next
// Extract element entry + item.
entry := (*index_entry)(elem.data)
@ -206,18 +211,21 @@ func (i *Index) get(key string, hook func(*indexed_item)) {
// Pass to hook.
hook(item)
})
// Set next.
elem = next
}
}
// key uses hasher to generate Key{} from given raw parts.
func (i *Index) key(buf *byteutil.Buffer, parts []unsafe.Pointer) string {
buf.B = buf.B[:0]
if len(parts) != len(i.fields) {
panicf("incorrect number key parts: want=%d received=%d",
len(i.fields),
len(parts),
)
}
buf.B = buf.B[:0]
if !allow_zero(i.flags) {
for x, field := range i.fields {
before := len(buf.B)
@ -301,8 +309,13 @@ func (i *Index) delete(key string, hook func(*indexed_item)) {
// Delete at hash.
i.data.Delete(key)
// Iterate entries in list.
l.rangefn(func(elem *list_elem) {
// Iterate the list.
for elem := l.head; //
elem != nil; //
{
// Get next before
// any modification.
next := elem.next
// Remove elem.
l.remove(elem)
@ -319,7 +332,10 @@ func (i *Index) delete(key string, hook func(*indexed_item)) {
// Pass to hook.
hook(item)
})
// Set next.
elem = next
}
// Release list.
free_list(l)
@ -375,17 +391,21 @@ type index_entry struct {
func new_index_entry() *index_entry {
v := index_entry_pool.Get()
if v == nil {
v = new(index_entry)
e := new(index_entry)
e.elem.data = unsafe.Pointer(e)
v = e
}
entry := v.(*index_entry)
ptr := unsafe.Pointer(entry)
entry.elem.data = ptr
return entry
}
// free_index_entry releases the index_entry.
func free_index_entry(entry *index_entry) {
entry.elem.data = nil
if entry.elem.next != nil ||
entry.elem.prev != nil {
should_not_reach()
return
}
entry.key = ""
entry.index = nil
entry.item = nil

View file

@ -24,18 +24,22 @@ type indexed_item struct {
func new_indexed_item() *indexed_item {
v := indexed_item_pool.Get()
if v == nil {
v = new(indexed_item)
i := new(indexed_item)
i.elem.data = unsafe.Pointer(i)
v = i
}
item := v.(*indexed_item)
ptr := unsafe.Pointer(item)
item.elem.data = ptr
return item
}
// free_indexed_item releases the indexed_item.
func free_indexed_item(item *indexed_item) {
item.elem.data = nil
item.indexed = item.indexed[:0]
if len(item.indexed) > 0 ||
item.elem.next != nil ||
item.elem.prev != nil {
should_not_reach()
return
}
item.data = nil
indexed_item_pool.Put(item)
}
@ -50,7 +54,7 @@ func (i *indexed_item) drop_index(entry *index_entry) {
continue
}
// Move all index entries down + reslice.
// Reslice index entries minus 'x'.
_ = copy(i.indexed[x:], i.indexed[x+1:])
i.indexed[len(i.indexed)-1] = nil
i.indexed = i.indexed[:len(i.indexed)-1]

View file

@ -40,9 +40,12 @@ func new_list() *list {
// free_list releases the list.
func free_list(list *list) {
list.head = nil
list.tail = nil
list.len = 0
if list.head != nil ||
list.tail != nil ||
list.len != 0 {
should_not_reach()
return
}
list_pool.Put(list)
}
@ -115,20 +118,27 @@ func (l *list) remove(elem *list_elem) {
elem.prev = nil
switch {
case next == nil:
if prev == nil {
// next == nil && prev == nil
//
// elem is ONLY one in list.
case next == nil && prev == nil:
l.head = nil
l.tail = nil
// elem is front in list.
case next != nil && prev == nil:
l.head = next
next.prev = nil
} else {
// next == nil && prev != nil
//
// elem is last in list.
case prev != nil && next == nil:
l.tail = prev
prev.next = nil
}
case prev == nil:
// next != nil && prev == nil
//
// elem is front in list.
l.head = next
next.prev = nil
// elem in middle of list.
default:
@ -139,17 +149,3 @@ func (l *list) remove(elem *list_elem) {
// Decr count
l.len--
}
// rangefn will range all elems in list, passing each to fn.
func (l *list) rangefn(fn func(*list_elem)) {
if fn == nil {
panic("nil fn")
}
for e := l.head; //
e != nil; //
{
n := e.next
fn(e)
e = n
}
}

180
vendor/codeberg.org/gruf/go-structr/ordered_list.bak generated vendored Normal file
View file

@ -0,0 +1,180 @@
package structr
import "sync"
type Timeline[StructType any, PK comparable] struct {
// hook functions.
pkey func(StructType) PK
gte func(PK, PK) bool
lte func(PK, PK) bool
copy func(StructType) StructType
// main underlying
// ordered item list.
list list
// indices used in storing passed struct
// types by user defined sets of fields.
indices []Index
// protective mutex, guards:
// - TODO
mutex sync.Mutex
}
func (t *Timeline[T, PK]) Init(config any) {
}
func (t *Timeline[T, PK]) Index(name string) *Index {
for i := range t.indices {
if t.indices[i].name == name {
return &t.indices[i]
}
}
panic("unknown index: " + name)
}
func (t *Timeline[T, PK]) Insert(values ...T) {
}
func (t *Timeline[T, PK]) LoadTop(min, max PK, length int, load func(min, max PK, length int) ([]T, error)) ([]T, error) {
// Allocate expected no. values.
values := make([]T, 0, length)
// Acquire lock.
t.mutex.Lock()
// Wrap unlock to only do once.
unlock := once(t.mutex.Unlock)
defer unlock()
// Check init'd.
if t.copy == nil {
panic("not initialized")
}
// Iterate through linked list from top (i.e. head).
for next := t.list.head; next != nil; next = next.next {
// Check if we've gathered
// enough values from timeline.
if len(values) >= length {
return values, nil
}
item := (*indexed_item)(next.data)
value := item.data.(T)
pkey := t.pkey(value)
// Check if below min.
if t.lte(pkey, min) {
continue
}
// Update min.
min = pkey
// Check if above max.
if t.gte(pkey, max) {
break
}
// Append value copy.
value = t.copy(value)
values = append(values, value)
}
}
func (t *Timeline[T, PK]) LoadBottom(min, max PK, length int, load func(min, max PK, length int) ([]T, error)) ([]T, error) {
// Allocate expected no. values.
values := make([]T, 0, length)
// Acquire lock.
t.mutex.Lock()
// Wrap unlock to only do once.
unlock := once(t.mutex.Unlock)
defer unlock()
// Check init'd.
if t.copy == nil {
panic("not initialized")
}
// Iterate through linked list from bottom (i.e. tail).
for next := t.list.tail; next != nil; next = next.prev {
// Check if we've gathered
// enough values from timeline.
if len(values) >= length {
return values, nil
}
item := (*indexed_item)(next.data)
value := item.data.(T)
pkey := t.pkey(value)
// Check if above max.
if t.gte(pkey, max) {
continue
}
// Update max.
max = pkey
// Check if below min.
if t.lte(pkey, min) {
break
}
// Append value copy.
value = t.copy(value)
values = append(values, value)
}
// Done with
// the lock.
unlock()
// Attempt to load values up to given length.
next, err := load(min, max, length-len(values))
if err != nil {
return nil, err
}
// Acquire lock.
t.mutex.Lock()
// Store uncached values.
for i := range next {
t.store_value(
nil, "",
uncached[i],
)
}
// Done with lock.
t.mutex.Unlock()
// Append uncached to return values.
values = append(values, next...)
return values, nil
}
func (t *Timeline[T, PK]) index(value T) *indexed_item {
pk := t.pkey(value)
switch {
case t.list.len == 0:
case pk < t.list.head.data:
}
}
func (t *Timeline[T, PK]) delete(item *indexed_item) {
}

View file

@ -68,9 +68,9 @@ func (q *Queue[T]) Init(config QueueConfig[T]) {
// Index selects index with given name from queue, else panics.
func (q *Queue[T]) Index(name string) *Index {
for i := range q.indices {
if q.indices[i].name == name {
return &q.indices[i]
for i, idx := range q.indices {
if idx.name == name {
return &(q.indices[i])
}
}
panic("unknown index: " + name)
@ -207,17 +207,17 @@ func (q *Queue[T]) Len() int {
// Debug returns debug stats about queue.
func (q *Queue[T]) Debug() map[string]any {
m := make(map[string]any)
m := make(map[string]any, 2)
q.mutex.Lock()
m["queue"] = q.queue.len
indices := make(map[string]any)
indices := make(map[string]any, len(q.indices))
m["indices"] = indices
for i := range q.indices {
for _, idx := range q.indices {
var n uint64
for _, l := range q.indices[i].data.m {
for _, l := range idx.data.m {
n += uint64(l.len)
}
indices[q.indices[i].name] = n
indices[idx.name] = n
}
q.mutex.Unlock()
return m

View file

@ -2,7 +2,10 @@
import (
"fmt"
"os"
"reflect"
"runtime"
"strings"
"unicode"
"unicode/utf8"
"unsafe"
@ -182,7 +185,32 @@ func deref(p unsafe.Pointer, n uint) unsafe.Pointer {
return p
}
// eface_data returns the data ptr from an empty interface.
func eface_data(a any) unsafe.Pointer {
type eface struct{ _, data unsafe.Pointer }
return (*eface)(unsafe.Pointer(&a)).data
}
// panicf provides a panic with string formatting.
func panicf(format string, args ...any) {
panic(fmt.Sprintf(format, args...))
}
// should_not_reach can be called to indicated a
// block of code should not be able to be reached,
// else it prints callsite info with a BUG report.
//
//go:noinline
func should_not_reach() {
pcs := make([]uintptr, 1)
_ = runtime.Callers(2, pcs)
fn := runtime.FuncForPC(pcs[0])
funcname := "go-structr" // by default use just our library name
if fn != nil {
funcname = fn.Name()
if i := strings.LastIndexByte(funcname, '/'); i != -1 {
funcname = funcname[i+1:]
}
}
os.Stderr.WriteString("BUG: assertion failed in " + funcname + "\n")
}

View file

@ -1,7 +1,5 @@
package structr
import "unsafe"
// once only executes 'fn' once.
func once(fn func()) func() {
var once int32
@ -13,9 +11,3 @@ func once(fn func()) func() {
fn()
}
}
// eface_data returns the data ptr from an empty interface.
func eface_data(a any) unsafe.Pointer {
type eface struct{ _, data unsafe.Pointer }
return (*eface)(unsafe.Pointer(&a)).data
}

2
vendor/modules.txt vendored
View file

@ -66,7 +66,7 @@ codeberg.org/gruf/go-storage/disk
codeberg.org/gruf/go-storage/internal
codeberg.org/gruf/go-storage/memory
codeberg.org/gruf/go-storage/s3
# codeberg.org/gruf/go-structr v0.8.10
# codeberg.org/gruf/go-structr v0.8.11
## explicit; go 1.21
codeberg.org/gruf/go-structr
# codeberg.org/superseriousbusiness/exif-terminator v0.9.0

119
web/source/css/login.css Normal file
View file

@ -0,0 +1,119 @@
/*
GoToSocial
Copyright (C) GoToSocial Authors admin@gotosocial.org
SPDX-License-Identifier: AGPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
.about-section.settings {
display: flex;
flex-direction: row;
gap: 1rem;
align-items: center;
justify-content: center;
padding-top: 1rem !important;
padding-bottom: 1rem !important;
p.settings-text {
margin-top: auto;
margin-bottom: auto;
flex: auto;
}
.settings-button {
flex: auto;
}
}
/*
Reuse about styling, but rework it
to separate sections a bit more.
*/
.about {
display: flex;
flex-direction: column;
gap: 2rem;
padding: 0;
background: initial;
box-shadow: initial;
border: initial;
border-radius: initial;
.about-section {
padding: 2rem;
background: $bg-accent;
box-shadow: $boxshadow;
border: $boxshadow-border;
border-radius: $br;
h3 {
margin-top: 0px;
}
}
}
.apps {
align-self: start;
.applist {
margin: 0;
padding: 0;
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 0.5rem;
align-content: start;
.applist-entry {
display: grid;
grid-template-columns: 25% 1fr;
grid-template-areas: "logo text";
gap: 1.5rem;
padding: 0.5rem;
.applist-logo {
grid-area: logo;
align-self: center;
justify-self: center;
width: 100%;
object-fit: contain;
flex: 1 1 auto;
}
.applist-logo.redraw {
fill: $fg;
stroke: $fg;
}
.applist-text {
grid-area: text;
a {
font-weight: bold;
}
}
}
}
}
@media screen and (max-width: 600px) {
.apps .applist {
grid-template-columns: 1fr;
}
}

View file

@ -116,3 +116,9 @@
text-align: center;
}
}
.login {
position: absolute;
top: 2vh;
right: 2vh;
}

28
web/template/login.tmpl Normal file
View file

@ -0,0 +1,28 @@
{{- /*
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ -}}
<main class="about">
<section role="region" class="about-section settings">
<p class="settings-text">
Looking to configure your profile and other settings?
</p>
<a href="/settings" class="settings-button button with-icon">Settings</a>
</section>
{{- include "index_apps.tmpl" . | indent 1 }}
</main>

View file

@ -0,0 +1,22 @@
{{- /*
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ -}}
{{- if .showLoginButton }}
<div class="login"><a href="/login" class="button with-icon">Log in</a></div>
{{- end }}

View file

@ -71,7 +71,9 @@ image/webp
{{- end }}
<title>{{- template "instanceTitle" . -}}</title>
</head>
<body class="page">
<body>
{{- include "login_button.tmpl" . | indent 3 }}
<div class="page">
<header class="page-header">
{{- include "page_header.tmpl" . | indent 3 }}
</header>
@ -81,5 +83,6 @@ image/webp
<footer class="page-footer">
{{- include "page_footer.tmpl" . | indent 3 }}
</footer>
</div>
</body>
</html>