mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 23:32:25 -05:00 
			
		
		
		
	* 🐸restructure frontend stuff, include admin and future user panel in main repo, properly deduplicate bundles for css+js across uses
* rename bundled to dist, caught by gitignore
* re-include status.css for profile template
* default to localhost
* serve frontend panels
* add todo message for abstraction
* refactor oauth registration flow
* oauth restructure
* update footer template
* change panel routes
* remove superfluous css imports
* write bundle to disk from test server, use forked budo-express
* wrap all page content in container
for robustness with addons etc injection other elements in body
* update documentation, goreleaser, Dockerfile
* update template meta tags
* add AGPL-3.0+ license header everywhere
* only attach update listener on EventEmitter
* cleaner config for various frontend bundles
* fix bundler script paths
* Merge commit 'd191931932'
* fix up dockerfile, goreleaser
* go mod tidy
* add uglifyify
* move status hide/show js to frontend bundle
* fix stylesheet color( func regressions
* update contributing docs for new build path
* update goreleaser + docker building
* resolve dependency paths properly
* update package name
* use api errorhandler
Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
		
	
			
		
			
				
	
	
		
			198 lines
		
	
	
	
		
			5.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			198 lines
		
	
	
	
		
			5.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
|    GoToSocial
 | |
|    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | |
| 
 | |
|    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 (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io/ioutil"
 | |
| 	"net/http"
 | |
| 	"path/filepath"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/gin-gonic/gin"
 | |
| 	"github.com/superseriousbusiness/gotosocial/internal/api"
 | |
| 	"github.com/superseriousbusiness/gotosocial/internal/config"
 | |
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 | |
| 	"github.com/superseriousbusiness/gotosocial/internal/processing"
 | |
| 	"github.com/superseriousbusiness/gotosocial/internal/router"
 | |
| 	"github.com/superseriousbusiness/gotosocial/internal/uris"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	confirmEmailPath = "/" + uris.ConfirmEmailPath
 | |
| 	tokenParam       = "token"
 | |
| 	usernameKey      = "username"
 | |
| 	statusIDKey      = "status"
 | |
| 	profilePath      = "/@:" + usernameKey
 | |
| 	statusPath       = profilePath + "/statuses/:" + statusIDKey
 | |
| )
 | |
| 
 | |
| // Module implements the api.ClientModule interface for web pages.
 | |
| type Module struct {
 | |
| 	processor      processing.Processor
 | |
| 	assetsPath     string
 | |
| 	adminPath      string
 | |
| 	defaultAvatars []string
 | |
| }
 | |
| 
 | |
| // New returns a new api.ClientModule for web pages.
 | |
| func New(processor processing.Processor) (api.ClientModule, error) {
 | |
| 	assetsBaseDir := config.GetWebAssetBaseDir()
 | |
| 	if assetsBaseDir == "" {
 | |
| 		return nil, fmt.Errorf("%s cannot be empty and must be a relative or absolute path", config.WebAssetBaseDirFlag())
 | |
| 	}
 | |
| 
 | |
| 	assetsPath, err := filepath.Abs(assetsBaseDir)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("error getting absolute path of %s: %s", assetsBaseDir, err)
 | |
| 	}
 | |
| 
 | |
| 	defaultAvatarsPath := filepath.Join(assetsPath, "default_avatars")
 | |
| 	defaultAvatarFiles, err := ioutil.ReadDir(defaultAvatarsPath)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("error reading default avatars at %s: %s", defaultAvatarsPath, err)
 | |
| 	}
 | |
| 
 | |
| 	defaultAvatars := []string{}
 | |
| 	for _, f := range defaultAvatarFiles {
 | |
| 		// ignore directories
 | |
| 		if f.IsDir() {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// ignore files bigger than 50kb
 | |
| 		if f.Size() > 50000 {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		extension := strings.TrimPrefix(strings.ToLower(filepath.Ext(f.Name())), ".")
 | |
| 
 | |
| 		// take only files with simple extensions
 | |
| 		switch extension {
 | |
| 		case "svg", "jpeg", "jpg", "gif", "png":
 | |
| 			defaultAvatarPath := fmt.Sprintf("/assets/default_avatars/%s", f.Name())
 | |
| 			defaultAvatars = append(defaultAvatars, defaultAvatarPath)
 | |
| 		default:
 | |
| 			continue
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return &Module{
 | |
| 		processor:      processor,
 | |
| 		assetsPath:     assetsPath,
 | |
| 		adminPath:      filepath.Join(assetsPath, "admin"),
 | |
| 		defaultAvatars: defaultAvatars,
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| func (m *Module) baseHandler(c *gin.Context) {
 | |
| 	host := config.GetHost()
 | |
| 	instance, err := m.processor.InstanceGet(c.Request.Context(), host)
 | |
| 	if err != nil {
 | |
| 		api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	c.HTML(http.StatusOK, "index.tmpl", gin.H{
 | |
| 		"instance": instance,
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // TODO: abstract the {admin, user}panel handlers in some way
 | |
| func (m *Module) AdminPanelHandler(c *gin.Context) {
 | |
| 	host := config.GetHost()
 | |
| 	instance, err := m.processor.InstanceGet(c.Request.Context(), host)
 | |
| 	if err != nil {
 | |
| 		api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	c.HTML(http.StatusOK, "frontend.tmpl", gin.H{
 | |
| 		"instance": instance,
 | |
| 		"stylesheets": []string{
 | |
| 			"/assets/Fork-Awesome/css/fork-awesome.min.css",
 | |
| 			"/assets/dist/panels-admin-style.css",
 | |
| 		},
 | |
| 		"javascript": []string{
 | |
| 			"/assets/dist/bundle.js",
 | |
| 			"/assets/dist/admin-panel.js",
 | |
| 		},
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func (m *Module) UserPanelHandler(c *gin.Context) {
 | |
| 	host := config.GetHost()
 | |
| 	instance, err := m.processor.InstanceGet(c.Request.Context(), host)
 | |
| 	if err != nil {
 | |
| 		api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	c.HTML(http.StatusOK, "frontend.tmpl", gin.H{
 | |
| 		"instance": instance,
 | |
| 		"stylesheets": []string{
 | |
| 			"/assets/Fork-Awesome/css/fork-awesome.min.css",
 | |
| 			"/assets/dist/_colors.css",
 | |
| 			"/assets/dist/base.css",
 | |
| 			"/assets/dist/panels-user-style.css",
 | |
| 		},
 | |
| 		"javascript": []string{
 | |
| 			"/assets/dist/bundle.js",
 | |
| 			"/assets/dist/user-panel.js",
 | |
| 		},
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // Route satisfies the RESTAPIModule interface
 | |
| func (m *Module) Route(s router.Router) error {
 | |
| 	// serve static files from assets dir at /assets
 | |
| 	s.AttachStaticFS("/assets", fileSystem{http.Dir(m.assetsPath)})
 | |
| 
 | |
| 	s.AttachHandler(http.MethodGet, "/admin", m.AdminPanelHandler)
 | |
| 	// redirect /admin/ to /admin
 | |
| 	s.AttachHandler(http.MethodGet, "/admin/", func(c *gin.Context) {
 | |
| 		c.Redirect(http.StatusMovedPermanently, "/admin")
 | |
| 	})
 | |
| 
 | |
| 	s.AttachHandler(http.MethodGet, "/user", m.UserPanelHandler)
 | |
| 	// redirect /settings/ to /settings
 | |
| 	s.AttachHandler(http.MethodGet, "/user/", func(c *gin.Context) {
 | |
| 		c.Redirect(http.StatusMovedPermanently, "/user")
 | |
| 	})
 | |
| 
 | |
| 	// serve front-page
 | |
| 	s.AttachHandler(http.MethodGet, "/", m.baseHandler)
 | |
| 
 | |
| 	// serve profile pages at /@username
 | |
| 	s.AttachHandler(http.MethodGet, profilePath, m.profileGETHandler)
 | |
| 
 | |
| 	// serve statuses
 | |
| 	s.AttachHandler(http.MethodGet, statusPath, m.threadGETHandler)
 | |
| 
 | |
| 	// serve email confirmation page at /confirm_email?token=whatever
 | |
| 	s.AttachHandler(http.MethodGet, confirmEmailPath, m.confirmEmailGETHandler)
 | |
| 
 | |
| 	// 404 handler
 | |
| 	s.AttachNoRouteHandler(func(c *gin.Context) {
 | |
| 		api.ErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), m.processor.InstanceGet)
 | |
| 	})
 | |
| 
 | |
| 	return nil
 | |
| }
 |