├── .example.env
├── .github
├── FUNDING.yml
└── workflows
│ └── codeql-analysis.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── assets
└── logo-light.svg
├── beubo.go
├── beubo_test.go
├── cmd
└── beubo
│ ├── .gitkeep
│ └── main.go
├── database.go
├── go.mod
├── go.sum
├── pkg
├── .gitkeep
├── middleware
│ ├── access.go
│ ├── auth.go
│ ├── main.go
│ ├── plugin.go
│ ├── site.go
│ ├── throttle.go
│ └── whitelist.go
├── plugin
│ ├── helper.go
│ ├── loader.go
│ ├── page.go
│ ├── request.go
│ └── structs.go
├── routes
│ ├── admin.go
│ ├── auth.go
│ ├── base.go
│ ├── page.go
│ ├── plugin.go
│ ├── router.go
│ ├── setting.go
│ ├── site.go
│ └── user.go
├── structs
│ ├── README.md
│ ├── config.go
│ ├── page.go
│ ├── page
│ │ ├── component.go
│ │ ├── component
│ │ │ ├── button.go
│ │ │ ├── checkboxfield.go
│ │ │ ├── form.go
│ │ │ ├── functions.go
│ │ │ ├── hiddenfield.go
│ │ │ ├── radiofield.go
│ │ │ ├── selectfield.go
│ │ │ ├── table.go
│ │ │ ├── text.go
│ │ │ ├── textareafield.go
│ │ │ └── textfield.go
│ │ ├── menu.go
│ │ └── menu
│ │ │ └── default.go
│ ├── role.go
│ ├── session.go
│ ├── setting.go
│ ├── site.go
│ ├── theme.go
│ └── user.go
├── template
│ ├── page.go
│ ├── render.go
│ └── site.go
└── utility
│ ├── email.go
│ ├── errors.go
│ ├── requests.go
│ └── strings.go
├── plugin.go
├── plugins
├── .gitignore
└── README.md
├── routes.go
├── settings.go
└── themes
└── README.md
/.example.env:
--------------------------------------------------------------------------------
1 | ASSETS_DIR=
2 | THEME=
3 |
4 | ENVIRONMENT=local
5 |
6 | # Can be mysql,sqlite3
7 | DB_DRIVER=sqlite3
8 | DB_HOST=localhost
9 | DB_NAME=beubo
10 | DB_USER=test
11 | DB_PASSWORD=test
12 |
13 | TEST_USER=test@test.com
14 | TEST_PASS=Test1234!
15 |
16 | SESSION_KEY=
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: uberswe
2 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '27 11 * * 5'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 |
28 | strategy:
29 | fail-fast: false
30 | matrix:
31 | language: [ 'go' ]
32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
33 | # Learn more:
34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
35 |
36 | steps:
37 | - name: Checkout repository
38 | uses: actions/checkout@v2
39 |
40 | # Initializes the CodeQL tools for scanning.
41 | - name: Initialize CodeQL
42 | uses: github/codeql-action/init@v1
43 | with:
44 | languages: ${{ matrix.language }}
45 | # If you wish to specify custom queries, you can do so here or in a config file.
46 | # By default, queries listed here will override any specified in a config file.
47 | # Prefix the list here with "+" to use these queries and those in the config file.
48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
49 |
50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
51 | # If this step fails, then you should remove it and run the build manually (see below)
52 | - name: Autobuild
53 | uses: github/codeql-action/autobuild@v1
54 |
55 | # ℹ️ Command-line programs to run using the OS shell.
56 | # 📚 https://git.io/JvXDl
57 |
58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
59 | # and modify them (or add more) to build your code if your project
60 | # uses a compiled language
61 |
62 | #- run: |
63 | # make bootstrap
64 | # make release
65 |
66 | - name: Perform CodeQL Analysis
67 | uses: github/codeql-action/analyze@v1
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .idea
3 | godirectory
4 | godirectory.db
5 | beubo.db
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Beubo
2 | Thank you for wanting to help! I want to make contributing to this project as easy and transparent as possible, whether it's:
3 |
4 | - Reporting a bug
5 | - Discussing the current state of the code
6 | - Submitting a fix
7 | - Proposing new features
8 | - Becoming a maintainer
9 |
10 | ## Developed with Github
11 | Create and discuss changes via Github. Please open an issue if you would like to report a bug or suggest new features and improvements. If you would like to contribute code please open a pull request. See [SECURITY.md](https://github.com/uberswe/beubo/blob/master/SECURITY.md) for directions on how to report security vulnerabilities.
12 |
13 | ## Any contributions you make will be under the MIT Software License
14 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
15 |
16 | ## Report bugs using Github's [issues](https://github.com/uberswe/beubo/issues)
17 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/uberswe/beubo/issues/new).
18 |
19 | ## Ensure your code is well formatted
20 | I use [lint](https://github.com/golang/lint) to ensure code is formatted well.
21 |
22 | ## License
23 | By contributing, you agree that your contributions will be licensed under its MIT License.
24 |
25 | ## References
26 |
27 |
28 | This document was created from [this template](https://gist.github.com/briandk/3d2e8b3ec8daf5a27a62) which was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Markus Tenghamn
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Beubo
2 |
3 | 
4 |
5 | **Beubo is in Alpha and not recommended for production use, expect breaking changes and bugs**
6 |
7 | I created Beubo to get better at Go. To learn more and to make it easier to get going with
8 | new projects. None of the platforms or libraries in the Go ecosystem felt right for me.
9 | That's why I set out to make my own CMS/Library, Beubo.
10 |
11 | Beubo is a CMS that aims to be easy to use and written in Go. I wanted it
12 | to be as easy to use as Wordpress but with much better performance and with support
13 | for multiple websites right from the start. I try to keep the capabilities of Beubo
14 | as small as possible. I hope I can make Beubo easy to build on using plugins so that
15 | it can be used for anything and everything.
16 |
17 | Here are a few of the features I want to support:
18 | - Site management, routing based on domain
19 | - Page creation, editing, deletion
20 | - Themes
21 | - Plugins
22 | - User management with roles and permissions
23 |
24 | That's pretty much it.
25 |
26 | Simply run `go run cmd/beubo/main.go` to get started.
27 |
28 | ## Database
29 |
30 | Beubo uses [GORM](https://gorm.io/) to handle database operations. Currently I am supporting sqlite3 and mysql but other drivers may work but have not been tested.
31 |
32 | ## Installation
33 |
34 | When running Beubo for the first time an installation page will open at the specified port. The
35 | page asks for various details needed to configure your site including database details. You will
36 | need to create a database on a MariaDB server and provide details
37 | so that Beubo can connect to it. You can also use sqlite3 but I only recommend it for local development.
38 |
39 | Once the installation is complete it will no longer be available, delete the .env file to redo the
40 | installation process. To start with a fresh database simply truncate your current database and it will
41 | auto migrate and seed a fresh database.
42 |
43 | ## CLI options
44 |
45 | ```
46 | -port=8080 Allows you to specify which port Beubo should listen on
47 | ```
48 |
49 | ## Templating
50 |
51 | Beubo uses the go html templates to build pages. These templates use functions to render sections of
52 | content which plugins can hook into when a request is made.
53 |
54 | ## Plugins
55 |
56 | Beubo supports go plugins. Simply place your `.so` under `/plugins` and Beubo will try to load this
57 | plugin as it starts. A plugin will need to expose a `Register` method in order to run. Please see
58 | the example plugin to learn more.
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Currently all versions are supported with security updates.
6 |
7 | ## Reporting a Vulnerability
8 |
9 | Please contact me via email or a twitter DM, the contact information can be found on my Github profile https://github.com/uberswe
10 |
--------------------------------------------------------------------------------
/assets/logo-light.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/beubo.go:
--------------------------------------------------------------------------------
1 | package beubo
2 |
3 | import (
4 | "archive/zip"
5 | "bytes"
6 | "flag"
7 | "fmt"
8 | "github.com/manifoldco/promptui"
9 | "io"
10 | "io/ioutil"
11 | "log"
12 | "net/http"
13 | "os"
14 | "path/filepath"
15 | "strings"
16 | )
17 |
18 | var port = 3000
19 |
20 | // Init is called to start Beubo, this calls various other functions that initialises some basic settings
21 | func Init() {
22 | readCLIFlags()
23 | }
24 |
25 | // Run runs the main application
26 | func Run() {
27 | checkFiles()
28 | settingsInit()
29 | databaseInit()
30 | databaseSeed()
31 | loadPlugins()
32 | routesInit()
33 | }
34 |
35 | // readCLIFlags parses command line flags such as port number
36 | func readCLIFlags() {
37 | flag.IntVar(&port, "port", port, "The port you would like the application to listen on")
38 | flag.Parse()
39 | }
40 |
41 | // checkFiles checks if there is a theme present and will otherwise prompt to download that theme
42 | func checkFiles() {
43 | _, err := os.Stat("./themes")
44 | if os.IsNotExist(err) {
45 | if ask("There is no themes folder present, would you like to create it?") {
46 | err := os.Mkdir("./themes", 0755)
47 | if err != nil {
48 | log.Fatal(err)
49 | }
50 | }
51 | }
52 | if !hasDirectory("./themes/") {
53 | if ask("There is no theme installed, would you like to download the default theme?") {
54 | resp, err := http.Get("https://github.com/uberswe/beubo-default/archive/master.zip")
55 | if err != nil {
56 | log.Fatal(err)
57 | }
58 | defer resp.Body.Close()
59 |
60 | body, err := ioutil.ReadAll(resp.Body)
61 | if err != nil {
62 | log.Fatal(err)
63 | }
64 |
65 | zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
66 | if err != nil {
67 | log.Fatal(err)
68 | }
69 |
70 | // Read all the files from zip archive
71 | for _, zipFile := range zipReader.File {
72 | err := extractAndWriteFile(zipFile)
73 | if err != nil {
74 | log.Println(err)
75 | continue
76 | }
77 | }
78 | }
79 | }
80 | _, err = os.Stat("./themes/install")
81 | if os.IsNotExist(err) {
82 | if ask("There is no install theme installed, would you like to download the default install theme?") {
83 | resp, err := http.Get("https://github.com/uberswe/beubo-install/archive/master.zip")
84 | if err != nil {
85 | log.Fatal(err)
86 | }
87 | defer resp.Body.Close()
88 |
89 | body, err := ioutil.ReadAll(resp.Body)
90 | if err != nil {
91 | log.Fatal(err)
92 | }
93 |
94 | zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
95 | if err != nil {
96 | log.Fatal(err)
97 | }
98 |
99 | // Read all the files from zip archive
100 | for _, zipFile := range zipReader.File {
101 | err := extractAndWriteFile(zipFile)
102 | if err != nil {
103 | log.Println(err)
104 | continue
105 | }
106 | }
107 | }
108 | }
109 | }
110 |
111 | // ask prompts the user for an answer to a question via the command line. Returns true for yes and false for no
112 | func ask(question string) bool {
113 | prompt := promptui.Select{
114 | Label: fmt.Sprintf("%s [Yes/No]", question),
115 | Items: []string{"Yes", "No"},
116 | }
117 | _, result, err := prompt.Run()
118 | if err != nil {
119 | return false
120 | }
121 | if result == "Yes" {
122 | return true
123 | }
124 | return false
125 | }
126 |
127 | // hasDirectory takes a path and checks if a directory exists in the path
128 | func hasDirectory(path string) bool {
129 | files, err := ioutil.ReadDir(path)
130 | if err != nil {
131 | return false
132 | }
133 | for _, f := range files {
134 | if f.IsDir() {
135 | return true
136 | }
137 | }
138 | return false
139 | }
140 |
141 | func extractAndWriteFile(f *zip.File) error {
142 | dest := "./themes/"
143 | rc, err := f.Open()
144 | if err != nil {
145 | return err
146 | }
147 | defer func() {
148 | if err := rc.Close(); err != nil {
149 | panic(err)
150 | }
151 | }()
152 |
153 | filename := strings.Replace(f.Name, "beubo-default-master/", "default/", 1)
154 | filename = strings.Replace(filename, "beubo-install-master/", "install/", 1)
155 |
156 | path := filepath.Join(dest, filename)
157 |
158 | // Check for ZipSlip (Directory traversal)
159 | if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) {
160 | return fmt.Errorf("illegal file path: %s", path)
161 | }
162 |
163 | if f.FileInfo().IsDir() {
164 | os.MkdirAll(path, f.Mode())
165 | } else {
166 | os.MkdirAll(filepath.Dir(path), f.Mode())
167 | f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
168 | if err != nil {
169 | return err
170 | }
171 | defer func() {
172 | if err := f.Close(); err != nil {
173 | panic(err)
174 | }
175 | }()
176 |
177 | _, err = io.Copy(f, rc)
178 | if err != nil {
179 | return err
180 | }
181 | }
182 | return nil
183 | }
184 |
--------------------------------------------------------------------------------
/beubo_test.go:
--------------------------------------------------------------------------------
1 | package beubo
2 |
3 | import "testing"
4 |
5 | func TestSetSetting(t *testing.T) {
6 | key := "key"
7 | value := "value"
8 | expected := key
9 | if result := setSetting(key, value); result != expected {
10 | t.Errorf("setSetting = %q, expected %q", result, expected)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/cmd/beubo/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uberswe/beubo/7b0100af941c71b906e4a49c4e0e3857a04fd098/cmd/beubo/.gitkeep
--------------------------------------------------------------------------------
/cmd/beubo/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/uberswe/beubo"
4 |
5 | func init() {
6 | beubo.Init()
7 | }
8 |
9 | func main() {
10 | beubo.Run()
11 | }
12 |
--------------------------------------------------------------------------------
/database.go:
--------------------------------------------------------------------------------
1 | package beubo
2 |
3 | import (
4 | "fmt"
5 | "github.com/uberswe/beubo/pkg/plugin"
6 | "github.com/uberswe/beubo/pkg/structs"
7 | "github.com/uberswe/beubo/pkg/utility"
8 | "golang.org/x/crypto/bcrypt"
9 | "gorm.io/driver/mysql"
10 | "gorm.io/driver/sqlite"
11 | "gorm.io/gorm"
12 | "gorm.io/gorm/logger"
13 | "io/ioutil"
14 | "log"
15 | "os"
16 | "time"
17 | )
18 |
19 | var (
20 | seedEmail = "seed@beubo.com"
21 | seedPassword = "Beubo1234!"
22 | // TODO change this to a config
23 | shouldSeed = false
24 | shouldRefreshDatabase = false
25 | // DB is used to perform database queries globally. In the future this should probably
26 | // be changed so that database.go declares methods that can be used to perform types of
27 | // queries
28 | DB *gorm.DB
29 | )
30 |
31 | func setupDB() *gorm.DB {
32 | log.Println("Opening database")
33 | dialector := getDialector(databaseUser, databasePassword, databaseHost, databasePort, databaseName, databaseDriver)
34 | newLogger := logger.New(
35 | log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
36 | logger.Config{
37 | SlowThreshold: time.Second, // Slow SQL threshold
38 | LogLevel: logger.Silent, // Log level
39 | Colorful: true,
40 | },
41 | )
42 | config := gorm.Config{
43 | Logger: newLogger,
44 | }
45 | if databaseDriver == "sqlite3" {
46 | config = gorm.Config{
47 | DisableForeignKeyConstraintWhenMigrating: true,
48 | Logger: newLogger,
49 | }
50 | }
51 | db, err := gorm.Open(dialector, &config)
52 | utility.ErrorHandler(err, true)
53 | return db
54 | }
55 |
56 | func getDialector(user string, pass string, host string, port string, name string, driver string) gorm.Dialector {
57 | connectString := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", user, pass, host, port, name)
58 | dialector := mysql.Open(connectString)
59 | if driver == "sqlite3" {
60 | connectString = databaseName
61 | dialector = sqlite.Open(connectString)
62 | }
63 | return dialector
64 | }
65 |
66 | func databaseInit() {
67 | DB = setupDB()
68 |
69 | if shouldRefreshDatabase {
70 | type Result struct {
71 | DropQuery string
72 | }
73 | var result []Result
74 |
75 | log.Println("Dropping all database tables")
76 |
77 | if databaseDriver == "sqlite3" {
78 | DB.Raw("SELECT 'DROP TABLE IF EXISTS `' || name || '`;' as drop_query FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';").Scan(&result)
79 | } else {
80 | DB.Raw("SELECT concat('DROP TABLE IF EXISTS `', table_name, '`;') as drop_query FROM information_schema.tables WHERE table_schema = 'beubo';").Scan(&result)
81 | }
82 |
83 | for _, r := range result {
84 | DB.Exec(r.DropQuery)
85 | }
86 | }
87 |
88 | log.Println("Running database migrations")
89 |
90 | err := DB.AutoMigrate(
91 | &structs.User{},
92 | &structs.UserActivation{},
93 | &structs.Config{},
94 | &structs.Page{},
95 | &structs.Theme{},
96 | &structs.Session{},
97 | &structs.Site{},
98 | &structs.Tag{},
99 | &structs.Comment{},
100 | &structs.Setting{},
101 | &plugin.PluginSite{},
102 | &structs.Role{},
103 | &structs.Feature{})
104 | utility.ErrorHandler(err, true)
105 | }
106 |
107 | func prepareSeed(email string, password string) {
108 | shouldSeed = true
109 | seedEmail = email
110 | seedPassword = password
111 | }
112 |
113 | func databaseSeed() {
114 | theme := addThemes()
115 |
116 | // user registration is disabled by default
117 | disableRegistration := structs.Setting{Key: "enable_user_registration", Value: "false"}
118 | DB.Where("key = ?", disableRegistration.Key).First(&disableRegistration)
119 | if disableRegistration.ID == 0 {
120 | DB.Create(&disableRegistration)
121 | }
122 |
123 | // users who register should have a member role
124 | newUserRole := structs.Setting{Key: "new_user_role", Value: "Member"}
125 | DB.Where("key = ?", newUserRole.Key).First(&newUserRole)
126 | if newUserRole.ID == 0 {
127 | DB.Create(&newUserRole)
128 | }
129 |
130 | features := []*structs.Feature{
131 | {Key: "manage_sites"},
132 | {Key: "manage_pages"},
133 | {Key: "manage_users"},
134 | {Key: "manage_user_roles"},
135 | {Key: "manage_plugins"},
136 | {Key: "manage_settings"},
137 | }
138 |
139 | for _, feature := range features {
140 | DB.Where("key = ?", feature.Key).First(&feature)
141 | if feature.ID == 0 {
142 | DB.Create(&feature)
143 | }
144 | }
145 |
146 | // Add default roles if not exist
147 | adminRole := structs.Role{}
148 | DB.Where("name = ?", "Administrator").First(&adminRole)
149 | if adminRole.ID == 0 {
150 | adminRole = structs.Role{Name: "Administrator", Features: features}
151 | DB.Create(&adminRole)
152 | }
153 | role := structs.Role{}
154 | DB.Where("name = ?", "Member").First(&role)
155 | if role.ID == 0 {
156 | role = structs.Role{Name: "Member"}
157 | DB.Create(&role)
158 | }
159 |
160 | // Add the specified default test user if the environment is also not set to production
161 | if environment != "production" && testuser != "" && testpass != "" {
162 | var err error
163 |
164 | // ASVS 4.0 point 2.4.4 states cost should be at least 13 https://github.com/OWASP/ASVS/
165 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(testpass), 14)
166 |
167 | utility.ErrorHandler(err, true)
168 |
169 | user := structs.User{Email: testuser, Password: string(hashedPassword)}
170 | DB.Where("email = ?", user.Email).First(&user)
171 | if user.ID == 0 {
172 | user.Roles = []*structs.Role{
173 | &adminRole,
174 | }
175 | DB.Create(&user)
176 | }
177 | }
178 |
179 | log.Println("should seed", shouldSeed)
180 |
181 | // If seeding is enabled we perform the seed with default info
182 | if shouldSeed {
183 | seedData(theme, adminRole)
184 | }
185 |
186 | shouldSeed = false
187 | seedEmail = ""
188 | seedPassword = ""
189 | }
190 |
191 | func seedData(theme structs.Theme, adminRole structs.Role) {
192 | log.Println("Seeding database")
193 | var err error
194 |
195 | // Create a site
196 |
197 | site := structs.Site{
198 | Title: "Beubo",
199 | Domain: "beubo.localhost",
200 | Theme: theme,
201 | Type: 1,
202 | }
203 |
204 | DB.Create(&site)
205 |
206 | // Create a page
207 | content := `
Welcome to Beubo! Beubo is a free, simple, and minimal CMS with unlimited extensibility using plugins. This is the default page and can be changed in the admin area for this site.
`
208 | content += `
Beubo is open source and the project can be found on Github. If you find any problems or have an idea on how Beubo can be improved, please feel free to open an issue here.
`
209 | content += `
Feel free to open a pull request if you would like to contribute your own changes.
`
210 | content += `
For more information on how to use, customize and extend Beubo please see the wiki
`
211 |
212 | page := structs.Page{
213 | Model: gorm.Model{},
214 | Title: "Default page",
215 | Content: content,
216 | Slug: "/",
217 | Template: "page",
218 | SiteID: int(site.ID),
219 | }
220 | DB.Create(&page)
221 |
222 | // ASVS 4.0 point 2.4.4 states cost should be at least 13 https://github.com/OWASP/ASVS/
223 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(seedPassword), 14)
224 |
225 | utility.ErrorHandler(err, true)
226 |
227 | user := structs.User{Email: seedEmail, Password: string(hashedPassword)}
228 | DB.Where("email = ?", user.Email).First(&user)
229 | if user.ID == 0 {
230 | user.Roles = []*structs.Role{
231 | &adminRole,
232 | }
233 | DB.Create(&user)
234 | }
235 | }
236 |
237 | func addThemes() (theme structs.Theme) {
238 | // Add initial themes
239 | files, err := ioutil.ReadDir(rootDir)
240 | if err != nil {
241 | log.Fatal(err)
242 | }
243 | for _, file := range files {
244 | // Ignore the install directory, only used for installation
245 | if file.IsDir() && file.Name() != "install" {
246 | theme = structs.Theme{}
247 | DB.Where("slug = ?", file.Name()).First(&theme)
248 | if theme.ID == 0 {
249 | theme = structs.Theme{Slug: file.Name(), Title: file.Name()}
250 | DB.Create(&theme)
251 | }
252 | }
253 | }
254 | return theme
255 | }
256 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/uberswe/beubo
2 |
3 | go 1.15
4 |
5 | require (
6 | github.com/davecgh/go-spew v1.1.1 // indirect
7 | github.com/goincremental/negroni-sessions v0.0.0-20171223143234-40b49004abee
8 | github.com/gorilla/context v1.1.1 // indirect
9 | github.com/gorilla/mux v1.8.0
10 | github.com/gorilla/securecookie v1.1.1
11 | github.com/gorilla/sessions v1.2.1 // indirect
12 | github.com/joho/godotenv v1.3.0
13 | github.com/kr/pretty v0.2.0 // indirect
14 | github.com/lunixbochs/vtclean v1.0.0 // indirect
15 | github.com/manifoldco/promptui v0.8.0
16 | github.com/mattn/go-colorable v0.1.8 // indirect
17 | github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
18 | github.com/stretchr/stew v0.0.0-20130812190256-80ef0842b48b
19 | github.com/stretchr/testify v1.5.1 // indirect
20 | github.com/urfave/negroni v1.0.0
21 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
22 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777
23 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
24 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
25 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
26 | gopkg.in/yaml.v2 v2.2.8 // indirect
27 | gorm.io/driver/mysql v1.0.3
28 | gorm.io/driver/sqlite v1.1.4
29 | gorm.io/gorm v1.20.11
30 | )
31 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
2 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
3 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
4 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
5 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
6 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
11 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
12 | github.com/goincremental/negroni-sessions v0.0.0-20171223143234-40b49004abee h1:aBLa9mJSmSV0I7vZrb+jMakH/B5rR+mRI6Q/DxQ+O7g=
13 | github.com/goincremental/negroni-sessions v0.0.0-20171223143234-40b49004abee/go.mod h1:32Cq/6avji0Xp32YipwaOV3PsvpGfuYKTJP9XJrdXe8=
14 | github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
15 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
16 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
17 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
18 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
19 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
20 | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
21 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
22 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
23 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
24 | github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
25 | github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
26 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
27 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
28 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
29 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
30 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
31 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
32 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
33 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
34 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
35 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
36 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
37 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
38 | github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
39 | github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
40 | github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=
41 | github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
42 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
43 | github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd h1:Y4ZRx+RIPFlPL4gnD/I7bdqSNXHlNop1Q6NjQuHds00=
44 | github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
45 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
46 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
47 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
48 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
49 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
50 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
51 | github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
52 | github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
53 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
54 | github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
55 | github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
56 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
57 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
58 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
59 | github.com/stretchr/stew v0.0.0-20130812190256-80ef0842b48b h1:DmfFjW6pLdaJNVHfKgCxTdKFI6tM+0YbMd0kx7kE78s=
60 | github.com/stretchr/stew v0.0.0-20130812190256-80ef0842b48b/go.mod h1:yS/5aMz+lfJhykLjlAGbnhUhZIvVapOvtmk0MtzHktE=
61 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
62 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
63 | github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
64 | github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
65 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
66 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
67 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
68 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
69 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
70 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
71 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
72 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
73 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
74 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
75 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
76 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
77 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
78 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
79 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
80 | golang.org/x/sys v0.0.0-20201223074533-0d417f636930 h1:vRgIt+nup/B/BwIS0g2oC0haq0iqbV3ZA+u6+0TlNCo=
81 | golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
82 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
83 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
84 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
85 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
86 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
87 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
88 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
89 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
90 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
91 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
92 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
93 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
94 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
95 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
96 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
97 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
98 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
99 | gorm.io/driver/mysql v1.0.3 h1:+JKBYPfn1tygR1/of/Fh2T8iwuVwzt+PEJmKaXzMQXg=
100 | gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI=
101 | gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
102 | gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
103 | gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
104 | gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
105 | gorm.io/gorm v1.20.10 h1:oJDuZyiUVVH1t20aRv439xIhmx5HFqv3iDxwAZ5sBb0=
106 | gorm.io/gorm v1.20.10/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
107 | gorm.io/gorm v1.20.11 h1:jYHQ0LLUViV85V8dM1TP9VBBkfzKTnuTXDjYObkI6yc=
108 | gorm.io/gorm v1.20.11/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
109 |
--------------------------------------------------------------------------------
/pkg/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uberswe/beubo/7b0100af941c71b906e4a49c4e0e3857a04fd098/pkg/.gitkeep
--------------------------------------------------------------------------------
/pkg/middleware/access.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs"
5 | "gorm.io/gorm"
6 | "net/http"
7 | )
8 |
9 | // CanAccess checks if a user is allowed to access a specified feature
10 | func CanAccess(db *gorm.DB, FeatureKey string, r *http.Request) bool {
11 | self := r.Context().Value(UserContextKey)
12 | if self != nil && self.(structs.User).ID > 0 {
13 | if self.(structs.User).CanAccess(db, FeatureKey) {
14 | return true
15 | }
16 | }
17 | return false
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/middleware/auth.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "fmt"
5 | sessions "github.com/goincremental/negroni-sessions"
6 | "github.com/uberswe/beubo/pkg/structs"
7 | "golang.org/x/net/context"
8 | "log"
9 | "net/http"
10 | "strings"
11 | )
12 |
13 | // Auth checks if a user is authenticated and performs redirects if needed. The user struct is set to the request context if authenticated.
14 | func (bmw *BeuboMiddleware) Auth(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
15 | session := sessions.GetSession(r)
16 | token := session.Get("SES_ID")
17 |
18 | user := structs.FetchUserFromSession(bmw.DB, fmt.Sprintf("%v", token))
19 |
20 | // TODO in the future the admin path should be configurable and also not apply to every website
21 | if user.ID == 0 && strings.HasPrefix(r.URL.Path, "/admin") {
22 | log.Println("user is not logged in, redirect to /login")
23 | http.Redirect(rw, r, "/login", 302)
24 | return
25 | } else if user.ID > 0 {
26 | // If path is login then redirect to /admin
27 | if strings.HasPrefix(r.URL.Path, "/login") {
28 | http.Redirect(rw, r, "/admin", 302)
29 | return
30 | }
31 | ctx := context.WithValue(r.Context(), UserContextKey, user)
32 | r = r.WithContext(ctx)
33 | }
34 |
35 | next(rw, r)
36 | }
37 |
--------------------------------------------------------------------------------
/pkg/middleware/main.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/plugin"
5 | "gorm.io/gorm"
6 | )
7 |
8 | type key string
9 |
10 | const (
11 | // UserContextKey is used to fetch and store the user struct from context
12 | UserContextKey key = "user"
13 | // SiteContextKey is used to fetch and store the site struct from context
14 | SiteContextKey key = "site"
15 | )
16 |
17 | // BeuboMiddleware holds parameters relevant to Beubo middlewares
18 | type BeuboMiddleware struct {
19 | DB *gorm.DB
20 | PluginHandler *plugin.Handler
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/middleware/plugin.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import "net/http"
4 |
5 | // Plugin allows plugins to perform actions as a middleware
6 | func (bmw *BeuboMiddleware) Plugin(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
7 | bmw.PluginHandler.BeforeRequest(w, r)
8 | next.ServeHTTP(w, r)
9 | bmw.PluginHandler.AfterRequest(w, r)
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/middleware/site.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "fmt"
5 | "github.com/uberswe/beubo/pkg/structs"
6 | "golang.org/x/net/context"
7 | "net/http"
8 | )
9 |
10 | // Site determines if the domain is an existing site and performs relevant actions based on this
11 | func (bmw *BeuboMiddleware) Site(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
12 | site := structs.FetchSiteByHost(bmw.DB, r.Host)
13 | if site.ID == 0 {
14 | // No site detected
15 | // TODO maybe we should redirect or something if this is the case? Make it configurable
16 | } else {
17 | if site.Type == 3 {
18 | // The site is a redirect
19 | http.Redirect(rw, r, fmt.Sprintf("https://%s", site.DestinationDomain), 302)
20 | }
21 | // Site exists
22 | ctx := context.WithValue(r.Context(), SiteContextKey, site)
23 | r = r.WithContext(ctx)
24 | }
25 | next(rw, r)
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/middleware/throttle.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "golang.org/x/time/rate"
5 | "net/http"
6 | "sync"
7 | "time"
8 | )
9 |
10 | // Throttle holds relevant parameters for configuring how the throttle middleware behaves
11 | type Throttle struct {
12 | IPs map[string]ThrottleClient
13 | Mu *sync.RWMutex
14 | Rate rate.Limit
15 | Burst int
16 | Cleanup time.Duration
17 | }
18 |
19 | // ThrottleClient holds info about an ip such as it's rate limiter and the last time a request was made
20 | type ThrottleClient struct {
21 | Limiter *rate.Limiter
22 | last time.Time
23 | }
24 |
25 | func (t Throttle) add(ip string) *rate.Limiter {
26 | t.Mu.Lock()
27 | defer t.Mu.Unlock()
28 | limiter := rate.NewLimiter(t.Rate, t.Burst)
29 | t.IPs[ip] = ThrottleClient{
30 | Limiter: limiter,
31 | last: time.Now(),
32 | }
33 | return limiter
34 | }
35 |
36 | func (t Throttle) getLimiter(ip string) *rate.Limiter {
37 | t.Mu.Lock()
38 | c, exists := t.IPs[ip]
39 | if !exists {
40 | t.Mu.Unlock()
41 | return t.add(ip)
42 | }
43 | c.last = time.Now()
44 | t.IPs[ip] = c
45 | t.Mu.Unlock()
46 | return c.Limiter
47 | }
48 |
49 | func (t Throttle) cleanup() {
50 | t.Mu.Lock()
51 | for ip, v := range t.IPs {
52 | if time.Since(v.last) > t.Cleanup {
53 | delete(t.IPs, ip)
54 | }
55 | }
56 | t.Mu.Unlock()
57 | }
58 |
59 | // Throttle prevents multiple repeated requests in a certain time period
60 | func (t Throttle) Throttle(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
61 | // TODO if we are behind a load balancer we need to support X-Forwarded-For headers
62 | limiter := t.getLimiter(r.RemoteAddr)
63 | if !limiter.Allow() {
64 | http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
65 | return
66 | }
67 | next.ServeHTTP(w, r)
68 | }
69 |
--------------------------------------------------------------------------------
/pkg/middleware/whitelist.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | sessions "github.com/goincremental/negroni-sessions"
5 | "github.com/stretchr/stew/slice"
6 | "github.com/uberswe/beubo/pkg/structs"
7 | "log"
8 | "net"
9 | "net/http"
10 | "strings"
11 | )
12 |
13 | // Whitelist checks the ip whitelist configuration. If ip whitelisting is enabled, it ensures the ip is whitelisted when accessing administrator pages
14 | func (bmw *BeuboMiddleware) Whitelist(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
15 | settings := structs.FetchSettings(bmw.DB)
16 |
17 | whitelistEnabled := false
18 | var whitelistedIPs []string
19 |
20 | for _, s := range settings {
21 | if s.Key == "ip_whitelist" && s.Value == "true" {
22 | whitelistEnabled = true
23 | }
24 | if s.Key == "whitelisted_ip" {
25 | whitelistedIPs = append(whitelistedIPs, s.Value)
26 | }
27 | }
28 |
29 | // TODO if we are behind a load balancer we need to support X-Forwarded-For headers
30 | // TODO in the future the admin path should be configurable and also not apply to every website
31 | if strings.HasPrefix(r.URL.Path, "/admin") && whitelistEnabled && !slice.Contains(whitelistedIPs, hostWithoutPort(r.RemoteAddr)) {
32 | session := sessions.GetSession(r)
33 | session.Delete("SES_ID")
34 | session.Clear()
35 | log.Printf("IP %s is not whitelisted, redirect to /login\n", hostWithoutPort(r.RemoteAddr))
36 | http.Redirect(rw, r, "/login", 302)
37 | return
38 | }
39 | next(rw, r)
40 | }
41 |
42 | func hostWithoutPort(host string) string {
43 | if strings.Contains(host, ":") {
44 | h, _, err := net.SplitHostPort(host)
45 | if err == nil {
46 | return h
47 | }
48 | }
49 | return host
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/plugin/helper.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs"
5 | "net/http"
6 | )
7 |
8 | func (p Handler) isActive(r *http.Request, plugin Plugin) bool {
9 | site := structs.FetchSiteByHost(p.DB, r.Host)
10 | ps := FetchPluginSites(p.DB, plugin.Definition)
11 | if site.ID >= 0 {
12 | for _, pluginSite := range ps {
13 | if pluginSite.SiteID == site.ID && pluginSite.Active {
14 | return true
15 | }
16 | }
17 | }
18 | return false
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/plugin/loader.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs"
5 | "log"
6 | "path/filepath"
7 | "plugin"
8 | )
9 |
10 | // Load is a function that loads any available plugins in the plugins/ folder
11 | func Load(pluginHandler Handler) Handler {
12 | // The plugins (the *.so files) must be in a 'plugins' sub-directory
13 | allPlugins, err := filepath.Glob("plugins/*.so")
14 | if err != nil {
15 | panic(err)
16 | }
17 |
18 | log.Println("Loading plugins...")
19 | for _, filename := range allPlugins {
20 | plug, err := plugin.Open(filename)
21 | if err != nil {
22 | log.Println(err)
23 | continue
24 | }
25 |
26 | symbol, err := plug.Lookup("Register")
27 | if err != nil {
28 | log.Println("Plugin has no 'Register() map[string]string' function")
29 | continue
30 | }
31 |
32 | registerFunc, ok := symbol.(func() map[string]string)
33 | if !ok {
34 | log.Println("Plugin has no 'Register() map[string]string' function")
35 | continue
36 | }
37 |
38 | pluginData := registerFunc()
39 | log.Println(filename, pluginData)
40 |
41 | if pluginHandler.Plugins == nil {
42 | pluginHandler.Plugins = map[string]Plugin{}
43 | }
44 |
45 | pluginHandler.Plugins[filename] = Plugin{
46 | Plugin: plug,
47 | Definition: pluginData["identifier"],
48 | Data: pluginData,
49 | }
50 | }
51 |
52 | // TODO the following code requires an active database connection but what if a plugin wants to modify a connection? It would be good if this could be moved so the loading of plugins happens before the active check is performed.
53 | if pluginHandler.Plugins != nil {
54 | sites := structs.FetchSites(pluginHandler.DB)
55 | var plugins []string
56 | var existingSites []uint
57 | for _, s := range sites {
58 | existingSites = append(existingSites, s.ID)
59 | }
60 | for _, p := range pluginHandler.Plugins {
61 | plugins = append(plugins, p.Definition)
62 | for _, s := range sites {
63 | ps := PluginSite{}
64 | pluginHandler.DB.Where("plugin_identifier = ?", p.Definition).Find(&ps)
65 | if ps.ID <= 0 {
66 | ps.Active = false
67 | ps.SiteID = s.ID
68 | ps.PluginIdentifier = p.Definition
69 | pluginHandler.DB.Create(&p)
70 | }
71 | }
72 | }
73 | if len(plugins) > 0 {
74 | pluginHandler.DB.Where("plugin_identifier NOT IN ?", plugins).Delete([]PluginSite{})
75 | } else {
76 | // delete everything if there are no plugins
77 | pluginHandler.DB.Where("1=1").Delete([]PluginSite{})
78 | }
79 | if len(existingSites) > 0 {
80 | pluginHandler.DB.Where("site_id NOT IN ?", existingSites).Delete([]PluginSite{})
81 | } else {
82 | // delete everything if there are no sites
83 | pluginHandler.DB.Where("1=1").Delete([]PluginSite{})
84 | }
85 | }
86 |
87 | return pluginHandler
88 | }
89 |
--------------------------------------------------------------------------------
/pkg/plugin/page.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs"
5 | "net/http"
6 | )
7 |
8 | // PageData allows the modification of page data before it is passed to the execute function of the template handler
9 | func (p Handler) PageData(r *http.Request, pd structs.PageData) structs.PageData {
10 | for _, plug := range p.Plugins {
11 | if p.isActive(r, plug) {
12 | symbol, err := plug.Plugin.Lookup("PageData")
13 | if err != nil {
14 | continue
15 | }
16 |
17 | pageDataFunc, ok := symbol.(func(*http.Request, structs.PageData) structs.PageData)
18 | if !ok {
19 | continue
20 | }
21 | return pageDataFunc(r, pd)
22 | }
23 | }
24 | return pd
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/plugin/request.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | // BeforeRequest is called by Beubo as early as possible in the request stack after other middlewares have executed
8 | func (p Handler) BeforeRequest(w http.ResponseWriter, r *http.Request) {
9 | for _, plug := range p.Plugins {
10 | if p.isActive(r, plug) {
11 | symbol, err := plug.Plugin.Lookup("BeforeRequest")
12 | if err != nil {
13 | continue
14 | }
15 |
16 | beforeRequestFunc, ok := symbol.(func(http.ResponseWriter, *http.Request))
17 | if !ok {
18 | continue
19 | }
20 | beforeRequestFunc(w, r)
21 | }
22 | }
23 | }
24 |
25 | // AfterRequest is called by Beubo at the last possible place in the request stack before returning the response writer
26 | func (p Handler) AfterRequest(w http.ResponseWriter, r *http.Request) {
27 | for _, plug := range p.Plugins {
28 | if p.isActive(r, plug) {
29 | symbol, err := plug.Plugin.Lookup("AfterRequest")
30 | if err != nil {
31 | continue
32 | }
33 |
34 | afterRequestFunc, ok := symbol.(func(http.ResponseWriter, *http.Request))
35 | if !ok {
36 | continue
37 | }
38 | afterRequestFunc(w, r)
39 | }
40 | }
41 | }
42 |
43 | // PageHandler is called when a non-default route is called in Beubo, returning true will prevent any other handler from executing
44 | func (p Handler) PageHandler(w http.ResponseWriter, r *http.Request) (handled bool) {
45 | for _, plug := range p.Plugins {
46 | if p.isActive(r, plug) {
47 | symbol, err := plug.Plugin.Lookup("PageHandler")
48 | if err != nil {
49 | continue
50 | }
51 |
52 | pageHandlerFunc, ok := symbol.(func(http.ResponseWriter, *http.Request) (handled bool))
53 | if !ok {
54 | continue
55 | }
56 | return pageHandlerFunc(w, r)
57 | }
58 | }
59 | return false
60 | }
61 |
--------------------------------------------------------------------------------
/pkg/plugin/structs.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs"
5 | "gorm.io/gorm"
6 | "plugin"
7 | )
8 |
9 | // Handler holds all the needed information for plugins to function in Beubo
10 | type Handler struct {
11 | DB *gorm.DB
12 | Plugins map[string]Plugin
13 | }
14 |
15 | // Plugin represents data for a single plugin in Beubo
16 | type Plugin struct {
17 | Plugin *plugin.Plugin
18 | Definition string
19 | Data map[string]string
20 | }
21 |
22 | // PluginSite defines which site a plugin is activated for
23 | type PluginSite struct {
24 | gorm.Model
25 | Site structs.Site
26 | SiteID uint
27 | PluginIdentifier string
28 | Active bool
29 | }
30 |
31 | // FetchPluginSites gets a PluginSite definition based on the specified plugin identifier
32 | func FetchPluginSites(db *gorm.DB, plugin string) (ps []PluginSite) {
33 | db.Preload("Site").Where("plugin_identifier = ?", plugin).Find(&ps)
34 | return ps
35 | }
36 |
--------------------------------------------------------------------------------
/pkg/routes/admin.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "fmt"
5 | "github.com/uberswe/beubo/pkg/middleware"
6 | "github.com/uberswe/beubo/pkg/plugin"
7 | "github.com/uberswe/beubo/pkg/structs"
8 | "github.com/uberswe/beubo/pkg/structs/page"
9 | "github.com/uberswe/beubo/pkg/structs/page/component"
10 | "github.com/uberswe/beubo/pkg/utility"
11 | "html"
12 | "html/template"
13 | "net/http"
14 | )
15 |
16 | // Admin is the default admin route and template
17 | func (br *BeuboRouter) Admin(w http.ResponseWriter, r *http.Request) {
18 | if !middleware.CanAccess(br.DB, "manage_sites", r) {
19 | w.WriteHeader(http.StatusForbidden)
20 | return
21 | }
22 |
23 | var sites []structs.Site
24 |
25 | if err := br.DB.Find(&sites).Error; err != nil {
26 | utility.ErrorHandler(err, false)
27 | }
28 |
29 | var rows []component.Row
30 |
31 | self := r.Context().Value(middleware.UserContextKey)
32 | if self != nil && self.(structs.User).ID > 0 {
33 | for _, site := range sites {
34 | if !self.(structs.User).CanAccessSite(br.DB, site) {
35 | continue
36 | }
37 | sid := fmt.Sprintf("%d", site.ID)
38 | rows = append(rows, component.Row{
39 | Columns: []component.Column{
40 | {Name: "ID", Value: sid},
41 | {Name: "Site", Value: html.EscapeString(site.Title)},
42 | {Name: "Domain", Value: html.EscapeString(site.Domain)},
43 | {Name: "", Field: component.Button{
44 | Link: template.URL(fmt.Sprintf("%s://%s/", "https", html.EscapeString(site.Domain))),
45 | Class: "btn btn-primary",
46 | Content: "View",
47 | T: br.Renderer.T,
48 | }},
49 | {Name: "", Field: component.Button{
50 | Link: template.URL(fmt.Sprintf("/admin/sites/a/%s", sid)),
51 | Class: "btn btn-primary",
52 | Content: "Manage",
53 | T: br.Renderer.T,
54 | }},
55 | {Name: "", Field: component.Button{
56 | Link: template.URL(fmt.Sprintf("/admin/sites/edit/%s", sid)),
57 | Class: "btn btn-primary",
58 | Content: "Edit",
59 | T: br.Renderer.T,
60 | }},
61 | {Name: "", Field: component.Button{
62 | Link: template.URL(fmt.Sprintf("/admin/sites/delete/%s", sid)),
63 | Class: "btn btn-primary",
64 | Content: "Delete",
65 | T: br.Renderer.T,
66 | }},
67 | },
68 | })
69 | }
70 | }
71 |
72 | table := component.Table{
73 | Section: "main",
74 | Header: []component.Column{
75 | {Name: "ID"},
76 | {Name: "Site"},
77 | {Name: "Domain"},
78 | {Name: ""},
79 | {Name: ""},
80 | {Name: ""},
81 | {Name: ""},
82 | },
83 | Rows: rows,
84 | PageNumber: 1,
85 | PageDisplayCount: 10,
86 | T: br.Renderer.T,
87 | }
88 |
89 | pageData := structs.PageData{
90 | Template: "admin.page",
91 | Title: "Admin - Sites",
92 | Components: []page.Component{
93 | component.Button{
94 | Section: "main",
95 | Link: template.URL("/admin/sites/add"),
96 | Class: "btn btn-primary",
97 | Content: "Add Site",
98 | T: br.Renderer.T,
99 | },
100 | table,
101 | },
102 | }
103 |
104 | br.Renderer.RenderHTMLPage(w, r, pageData)
105 | }
106 |
107 | // Settings is the route for loading the admin settings page
108 | func (br *BeuboRouter) Settings(w http.ResponseWriter, r *http.Request) {
109 | if !middleware.CanAccess(br.DB, "manage_settings", r) {
110 | w.WriteHeader(http.StatusForbidden)
111 | return
112 | }
113 |
114 | var settings []structs.Setting
115 |
116 | if err := br.DB.Find(&settings).Error; err != nil {
117 | utility.ErrorHandler(err, false)
118 | }
119 |
120 | var rows []component.Row
121 | for _, setting := range settings {
122 | sid := fmt.Sprintf("%d", setting.ID)
123 | rows = append(rows, component.Row{
124 | Columns: []component.Column{
125 | {Name: "ID", Value: sid},
126 | {Name: "Site", Value: html.EscapeString(setting.Key)},
127 | {Name: "Domain", Value: html.EscapeString(setting.Value)},
128 | {Name: "", Field: component.Button{
129 | Link: template.URL(fmt.Sprintf("/admin/settings/edit/%s", sid)),
130 | Class: "btn btn-primary",
131 | Content: "Edit",
132 | T: br.Renderer.T,
133 | }},
134 | {Name: "", Field: component.Button{
135 | Link: template.URL(fmt.Sprintf("/admin/settings/delete/%s", sid)),
136 | Class: "btn btn-primary",
137 | Content: "Delete",
138 | T: br.Renderer.T,
139 | }},
140 | },
141 | })
142 | }
143 |
144 | table := component.Table{
145 | Section: "main",
146 | Header: []component.Column{
147 | {Name: "ID"},
148 | {Name: "Key"},
149 | {Name: "Value"},
150 | {Name: ""},
151 | {Name: ""},
152 | },
153 | Rows: rows,
154 | PageNumber: 1,
155 | PageDisplayCount: 10,
156 | T: br.Renderer.T,
157 | }
158 |
159 | pageData := structs.PageData{
160 | Template: "admin.page",
161 | Title: "Admin - Settings",
162 | Components: []page.Component{
163 | component.Button{
164 | Section: "main",
165 | Link: template.URL("/admin/settings/add"),
166 | Class: "btn btn-primary",
167 | Content: "Add Setting",
168 | T: br.Renderer.T,
169 | },
170 | table,
171 | component.Text{
172 | Section: "main",
173 | Content: "This is the very early stages of how settings works. Currently it is just plain key and values with no checks. " +
174 | "To use IP whitelisting first set any IPs to whitelist with whitelisted_ip as the key and the IP as value. " +
175 | "Then set ip_whitelist to true to enable the whitelist blocking",
176 | T: br.Renderer.T,
177 | },
178 | },
179 | }
180 | br.Renderer.RenderHTMLPage(w, r, pageData)
181 | }
182 |
183 | // Users is the route for loading the admin users page
184 | func (br *BeuboRouter) Users(w http.ResponseWriter, r *http.Request) {
185 | if !middleware.CanAccess(br.DB, "manage_users", r) {
186 | w.WriteHeader(http.StatusForbidden)
187 | return
188 | }
189 |
190 | var users []structs.User
191 |
192 | if err := br.DB.Find(&users).Error; err != nil {
193 | utility.ErrorHandler(err, false)
194 | }
195 |
196 | self := r.Context().Value(middleware.UserContextKey)
197 |
198 | var rows []component.Row
199 | if self != nil && self.(structs.User).ID > 0 {
200 | for _, user := range users {
201 | sid := fmt.Sprintf("%d", user.ID)
202 | if self.(structs.User).ID == user.ID {
203 | rows = append(rows, component.Row{
204 | Columns: []component.Column{
205 | {Name: "ID", Value: sid},
206 | {Name: "Email", Value: user.Email},
207 | {Name: "", Field: component.Button{
208 | Link: template.URL(fmt.Sprintf("/admin/users/edit/%s", sid)),
209 | Class: "btn btn-primary",
210 | Content: "Edit",
211 | T: br.Renderer.T,
212 | }},
213 | {},
214 | },
215 | })
216 | } else {
217 | rows = append(rows, component.Row{
218 | Columns: []component.Column{
219 | {Name: "ID", Value: sid},
220 | {Name: "Email", Value: user.Email},
221 | {Name: "", Field: component.Button{
222 | Link: template.URL(fmt.Sprintf("/admin/users/edit/%s", sid)),
223 | Class: "btn btn-primary",
224 | Content: "Edit",
225 | T: br.Renderer.T,
226 | }},
227 | {Name: "", Field: component.Button{
228 | Link: template.URL(fmt.Sprintf("/admin/users/delete/%s", sid)),
229 | Class: "btn btn-primary",
230 | Content: "Delete",
231 | T: br.Renderer.T,
232 | }},
233 | },
234 | })
235 | }
236 | }
237 | }
238 |
239 | table := component.Table{
240 | Section: "main",
241 | Header: []component.Column{
242 | {Name: "ID"},
243 | {Name: "Email"},
244 | {Name: ""},
245 | {Name: ""},
246 | },
247 | Rows: rows,
248 | PageNumber: 1,
249 | PageDisplayCount: 10,
250 | T: br.Renderer.T,
251 | }
252 |
253 | pageData := structs.PageData{
254 | Template: "admin.page",
255 | Title: "Admin - Users",
256 | Components: []page.Component{
257 | component.Button{
258 | Section: "main",
259 | Link: template.URL("/admin/users/add"),
260 | Class: "btn btn-primary",
261 | Content: "Add User",
262 | T: br.Renderer.T,
263 | },
264 | table,
265 | },
266 | }
267 |
268 | br.Renderer.RenderHTMLPage(w, r, pageData)
269 |
270 | }
271 |
272 | // Plugins is the route for loading the admin plugins page
273 | func (br *BeuboRouter) Plugins(w http.ResponseWriter, r *http.Request) {
274 | if !middleware.CanAccess(br.DB, "manage_plugins", r) {
275 | w.WriteHeader(http.StatusForbidden)
276 | return
277 | }
278 |
279 | if r.Method == http.MethodPost {
280 | // TODO handle post
281 | return
282 | }
283 |
284 | // reload plugins when viewing the plugin page
285 | plh := plugin.Load(*br.PluginHandler)
286 | br.PluginHandler = &plh
287 |
288 | var rows []component.Row
289 | for _, p := range br.PluginHandler.Plugins {
290 | comprow := component.Row{
291 | Columns: []component.Column{
292 | {Name: "Name", Value: fmt.Sprintf("%s", p.Definition, p.Definition)},
293 | },
294 | }
295 | rows = append(rows, comprow)
296 | }
297 |
298 | table := component.Table{
299 | Section: "main",
300 | Header: []component.Column{
301 | {Name: "Name"},
302 | },
303 | Rows: rows,
304 | PageNumber: 1,
305 | PageDisplayCount: 10,
306 | T: br.Renderer.T,
307 | }
308 |
309 | pageData := structs.PageData{
310 | Template: "admin.page",
311 | Title: "Admin - Plugins",
312 | Components: []page.Component{
313 | table,
314 | },
315 | }
316 |
317 | br.Renderer.RenderHTMLPage(w, r, pageData)
318 | }
319 |
--------------------------------------------------------------------------------
/pkg/routes/auth.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | sessions "github.com/goincremental/negroni-sessions"
5 | "github.com/uberswe/beubo/pkg/structs"
6 | "github.com/uberswe/beubo/pkg/utility"
7 | "net/http"
8 | )
9 |
10 | // Login is the default login route
11 | func (br *BeuboRouter) Login(w http.ResponseWriter, r *http.Request) {
12 | pageData := structs.PageData{
13 | Template: "login",
14 | Title: "Login",
15 | }
16 |
17 | br.Renderer.RenderHTMLPage(w, r, pageData)
18 | }
19 |
20 | // LoginPost handles authentication via post request and verifies a username/password via the database
21 | func (br *BeuboRouter) LoginPost(w http.ResponseWriter, r *http.Request) {
22 | session := sessions.GetSession(r)
23 |
24 | invalidError := "Email or password incorrect, please try again or contact support"
25 |
26 | email := r.FormValue("email")
27 | password := r.FormValue("password")
28 |
29 | user := structs.AuthUser(br.DB, email, password)
30 |
31 | if user == nil || user.ID == 0 {
32 | utility.SetFlash(w, "error", []byte(invalidError))
33 | http.Redirect(w, r, "/login", 302)
34 | return
35 | }
36 |
37 | ses := structs.CreateSession(br.DB, int(user.ID))
38 |
39 | if ses.ID == 0 {
40 | w.WriteHeader(http.StatusInternalServerError)
41 | _, _ = w.Write([]byte("500 - Could not create session"))
42 | return
43 | }
44 |
45 | session.Set("SES_ID", ses.Token)
46 | http.Redirect(w, r, "/admin/", 302)
47 | }
48 |
49 | // Register renders the default registration page
50 | func (br *BeuboRouter) Register(w http.ResponseWriter, r *http.Request) {
51 | setting := structs.FetchSettingByKey(br.DB, "enable_user_registration")
52 | if setting.ID == 0 || setting.Value == "false" {
53 | // Registration is not allowed
54 | http.Redirect(w, r, "/login", 302)
55 | return
56 | }
57 |
58 | pageData := structs.PageData{
59 | Template: "register",
60 | Title: "Register",
61 | }
62 |
63 | br.Renderer.RenderHTMLPage(w, r, pageData)
64 | }
65 |
66 | // RegisterPost handles a registration request and inserts the user into the database
67 | func (br *BeuboRouter) RegisterPost(w http.ResponseWriter, r *http.Request) {
68 | setting := structs.FetchSettingByKey(br.DB, "enable_user_registration")
69 | if setting.ID == 0 || setting.Value == "false" {
70 | // Registration is not allowed
71 | http.Redirect(w, r, "/login", 302)
72 | return
73 | }
74 |
75 | invalidError := "Please make sure the email is correct or that it does not already belong to a registered account"
76 |
77 | email := r.FormValue("email")
78 | password := r.FormValue("password")
79 | roles := []*structs.Role{}
80 | sites := []*structs.Site{}
81 |
82 | if !utility.IsEmailValid(email) {
83 | utility.SetFlash(w, "error", []byte(invalidError))
84 | http.Redirect(w, r, "/login", 302)
85 | return
86 | }
87 |
88 | // TODO default role should be added on register
89 | if !structs.CreateUser(br.DB, email, password, roles, sites) {
90 | utility.SetFlash(w, "error", []byte(invalidError))
91 | http.Redirect(w, r, "/login", 302)
92 | return
93 | }
94 |
95 | utility.SetFlash(w, "message", []byte("Registration success, please check your email for further instructions"))
96 |
97 | http.Redirect(w, r, "/login", 302)
98 | }
99 |
100 | // Logout handles a GET logout request and removes the user session
101 | func (br *BeuboRouter) Logout(w http.ResponseWriter, r *http.Request) {
102 | session := sessions.GetSession(r)
103 | session.Delete("SES_ID")
104 | session.Clear()
105 | http.Redirect(w, r, "/", 302)
106 | }
107 |
--------------------------------------------------------------------------------
/pkg/routes/base.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs"
5 | beuboPage "github.com/uberswe/beubo/pkg/structs/page"
6 | "github.com/uberswe/beubo/pkg/structs/page/component"
7 | "html/template"
8 | "net/http"
9 | )
10 |
11 | // NotFoundHandler overrides the default not found handler
12 | func (br *BeuboRouter) NotFoundHandler(w http.ResponseWriter, r *http.Request) {
13 | pageData := structs.PageData{
14 | Template: "404",
15 | Title: "404 Not Found",
16 | }
17 |
18 | w.Header().Set("Content-Type", "text/html")
19 | w.WriteHeader(http.StatusNotFound)
20 |
21 | br.Renderer.RenderHTMLPage(w, r, pageData)
22 | }
23 |
24 | // PageHandler checks if a page exists for the given slug
25 | func (br *BeuboRouter) PageHandler(w http.ResponseWriter, r *http.Request) {
26 | if !br.PluginHandler.PageHandler(w, r) {
27 | site := structs.FetchSiteByHost(br.DB, r.Host)
28 | if site.ID != 0 {
29 | page := structs.FetchPageBySiteIDAndSlug(br.DB, int(site.ID), r.URL.Path)
30 | if page.ID != 0 {
31 | pageData := structs.PageData{
32 | Template: "page",
33 | Title: page.Title,
34 | // TODO Components should be defined on the page edit page and defined in the db
35 | Components: []beuboPage.Component{component.Text{
36 | Content: template.HTML(page.Content),
37 | Theme: "",
38 | Template: "",
39 | Class: "",
40 | Section: "main",
41 | T: br.Renderer.T,
42 | }},
43 | }
44 |
45 | br.Renderer.RenderHTMLPage(w, r, pageData)
46 | return
47 | }
48 | }
49 | br.NotFoundHandler(w, r)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/routes/page.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/gorilla/mux"
7 | "github.com/uberswe/beubo/pkg/middleware"
8 | "github.com/uberswe/beubo/pkg/structs"
9 | "github.com/uberswe/beubo/pkg/utility"
10 | "log"
11 | "net/http"
12 | "strconv"
13 | )
14 |
15 | // SiteAdminPageNew is a route for creating new pages
16 | func (br *BeuboRouter) SiteAdminPageNew(w http.ResponseWriter, r *http.Request) {
17 | if !middleware.CanAccess(br.DB, "manage_pages", r) {
18 | w.WriteHeader(http.StatusForbidden)
19 | return
20 | }
21 |
22 | params := mux.Vars(r)
23 | siteID := params["id"]
24 | extra := map[string]string{
25 | "SiteID": siteID,
26 | }
27 | pageData := structs.PageData{
28 | Template: "admin.site.page.add",
29 | Templates: br.Renderer.GetPageTemplates(),
30 | Title: "Admin - Add Page",
31 | Extra: extra,
32 | }
33 |
34 | br.Renderer.RenderHTMLPage(w, r, pageData)
35 | }
36 |
37 | // SiteAdminPageNewPost is a route for handling the post request for creating new pages
38 | func (br *BeuboRouter) SiteAdminPageNewPost(w http.ResponseWriter, r *http.Request) {
39 | if !middleware.CanAccess(br.DB, "manage_pages", r) {
40 | w.WriteHeader(http.StatusForbidden)
41 | return
42 | }
43 |
44 | params := mux.Vars(r)
45 | siteID := params["id"]
46 |
47 | successMessage := "Page created"
48 | invalidError := "an error occurred and the site could not be created."
49 |
50 | siteIDInt, err := strconv.Atoi(siteID)
51 | if err != nil {
52 | invalidError = "Invalid site id"
53 | utility.SetFlash(w, "error", []byte(invalidError))
54 | http.Redirect(w, r, "/admin/sites", 302)
55 | return
56 | }
57 |
58 | title := r.FormValue("titleField")
59 | slug := r.FormValue("slugField")
60 | content := r.FormValue("contentField")
61 | template := r.FormValue("templateField")
62 | tags := r.FormValue("tagField")
63 | var tagSlice []structs.Tag
64 | err = json.Unmarshal([]byte(tags), &tagSlice)
65 | if err != nil {
66 | log.Println(err)
67 | }
68 |
69 | for i, tag := range tagSlice {
70 | tempTag := structs.Tag{}
71 | br.DB.Where("value = ?", tag.Value).First(&tempTag)
72 | if tempTag.ID == 0 {
73 | br.DB.Create(&tag)
74 | tagSlice[i] = tag
75 | } else {
76 | tagSlice[i] = tempTag
77 | }
78 | }
79 |
80 | if len(title) < 1 {
81 | invalidError = "The title is too short"
82 | utility.SetFlash(w, "error", []byte(invalidError))
83 | http.Redirect(w, r, fmt.Sprintf("/admin/sites/a/%s/page/new", siteID), 302)
84 | return
85 | }
86 |
87 | if structs.CreatePage(br.DB, title, slug, tagSlice, template, content, siteIDInt) {
88 | utility.SetFlash(w, "message", []byte(successMessage))
89 | http.Redirect(w, r, fmt.Sprintf("/admin/sites/a/%s", siteID), 302)
90 | return
91 | }
92 |
93 | utility.SetFlash(w, "error", []byte(invalidError))
94 | http.Redirect(w, r, fmt.Sprintf("/admin/sites/a/%s/page/new", siteID), 302)
95 | return
96 | }
97 |
98 | // AdminSitePageEdit is the route for editing a page
99 | func (br *BeuboRouter) AdminSitePageEdit(w http.ResponseWriter, r *http.Request) {
100 | if !middleware.CanAccess(br.DB, "manage_pages", r) {
101 | w.WriteHeader(http.StatusForbidden)
102 | return
103 | }
104 |
105 | params := mux.Vars(r)
106 | siteID := params["id"]
107 | pageID := params["pageId"]
108 |
109 | pageIDInt, err := strconv.Atoi(pageID)
110 | if err != nil {
111 | invalidError := "Invalid page id"
112 | utility.SetFlash(w, "error", []byte(invalidError))
113 | http.Redirect(w, r, "/admin/sites", 302)
114 | return
115 | }
116 |
117 | siteIDInt, err := strconv.Atoi(siteID)
118 | if err != nil {
119 | invalidError := "Invalid site id"
120 | utility.SetFlash(w, "error", []byte(invalidError))
121 | http.Redirect(w, r, "/admin/sites", 302)
122 | return
123 | }
124 |
125 | page := structs.FetchPage(br.DB, pageIDInt)
126 |
127 | site := structs.FetchSite(br.DB, siteIDInt)
128 |
129 | // This should not be a nil slice since we are json encoding it even if it is empty
130 | var tags []structs.JSONTag
131 |
132 | for _, tag := range page.Tags {
133 | tags = append(tags, structs.JSONTag{
134 | Value: tag.Value,
135 | })
136 | }
137 |
138 | var jsonTags []byte
139 | jsonTags, err = json.Marshal(tags)
140 | if err != nil {
141 | log.Println(err)
142 | }
143 |
144 | extra := map[string]string{
145 | "SiteID": siteID,
146 | "PageID": pageID,
147 | "Slug": page.Slug,
148 | "Title": page.Title,
149 | "Content": page.Content,
150 | "Template": page.Template,
151 | "SiteDomain": site.Domain,
152 | "Tags": string(jsonTags),
153 | }
154 |
155 | pageData := structs.PageData{
156 | Template: "admin.site.page.edit",
157 | Templates: br.Renderer.GetPageTemplates(),
158 | Title: "Admin - Edit Page",
159 | Extra: extra,
160 | }
161 |
162 | br.Renderer.RenderHTMLPage(w, r, pageData)
163 | }
164 |
165 | // AdminSitePageEditPost is the route for handling a post request to edit a page
166 | func (br *BeuboRouter) AdminSitePageEditPost(w http.ResponseWriter, r *http.Request) {
167 | if !middleware.CanAccess(br.DB, "manage_pages", r) {
168 | w.WriteHeader(http.StatusForbidden)
169 | return
170 | }
171 |
172 | params := mux.Vars(r)
173 | siteID := params["id"]
174 | pageID := params["pageId"]
175 |
176 | path := fmt.Sprintf("/admin/sites/a/%s", siteID)
177 |
178 | i, err := strconv.Atoi(siteID)
179 | utility.ErrorHandler(err, false)
180 |
181 | pageIDInt, err := strconv.Atoi(pageID)
182 | if err != nil {
183 | invalidError := "Invalid page id"
184 | utility.SetFlash(w, "error", []byte(invalidError))
185 | http.Redirect(w, r, path, 302)
186 | return
187 | }
188 |
189 | successMessage := "Page updated"
190 | invalidError := "an error occurred and the page could not be updated."
191 |
192 | title := r.FormValue("titleField")
193 | slug := r.FormValue("slugField")
194 | content := r.FormValue("contentField")
195 | template := r.FormValue("templateField")
196 | tags := r.FormValue("tagField")
197 | var tagSlice []structs.Tag
198 | err2 := json.Unmarshal([]byte(tags), &tagSlice)
199 | utility.ErrorHandler(err2, false)
200 |
201 | for i, tag := range tagSlice {
202 | tempTag := structs.Tag{}
203 | br.DB.Where("value = ?", tag.Value).First(&tempTag)
204 | if tempTag.ID == 0 {
205 | br.DB.Create(&tag)
206 | tagSlice[i] = tag
207 | } else {
208 | tagSlice[i] = tempTag
209 | }
210 | }
211 |
212 | if len(title) < 1 {
213 | invalidError = "The title is too short"
214 | utility.SetFlash(w, "error", []byte(invalidError))
215 | http.Redirect(w, r, fmt.Sprintf("%s/page/edit/%s", path, siteID), 302)
216 | return
217 | }
218 |
219 | if structs.UpdatePage(br.DB, i, title, slug, tagSlice, template, content, pageIDInt) {
220 | utility.SetFlash(w, "message", []byte(successMessage))
221 | http.Redirect(w, r, path, 302)
222 | return
223 | }
224 |
225 | utility.SetFlash(w, "error", []byte(invalidError))
226 | http.Redirect(w, r, path, 302)
227 | return
228 | }
229 |
230 | // AdminSitePageDelete is the route for handling the deletion of a pge
231 | func (br *BeuboRouter) AdminSitePageDelete(w http.ResponseWriter, r *http.Request) {
232 | if !middleware.CanAccess(br.DB, "manage_pages", r) {
233 | w.WriteHeader(http.StatusForbidden)
234 | return
235 | }
236 |
237 | params := mux.Vars(r)
238 | siteID := params["id"]
239 | pageID := params["pageId"]
240 |
241 | pageIDInt, err := strconv.Atoi(pageID)
242 |
243 | utility.ErrorHandler(err, false)
244 |
245 | structs.DeletePage(br.DB, pageIDInt)
246 |
247 | utility.SetFlash(w, "message", []byte("page deleted"))
248 |
249 | http.Redirect(w, r, fmt.Sprintf("/admin/sites/a/%s", siteID), 302)
250 | }
251 |
--------------------------------------------------------------------------------
/pkg/routes/plugin.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "fmt"
5 | "github.com/gorilla/mux"
6 | "github.com/uberswe/beubo/pkg/middleware"
7 | "github.com/uberswe/beubo/pkg/plugin"
8 | "github.com/uberswe/beubo/pkg/structs"
9 | "github.com/uberswe/beubo/pkg/structs/page"
10 | "github.com/uberswe/beubo/pkg/structs/page/component"
11 | "github.com/uberswe/beubo/pkg/utility"
12 | "html/template"
13 | "net/http"
14 | )
15 |
16 | // AdminPluginEdit is the route for editing a plugin
17 | func (br *BeuboRouter) AdminPluginEdit(w http.ResponseWriter, r *http.Request) {
18 | if !middleware.CanAccess(br.DB, "manage_plugins", r) {
19 | w.WriteHeader(http.StatusForbidden)
20 | return
21 | }
22 |
23 | params := mux.Vars(r)
24 | id := params["id"]
25 |
26 | plugins := plugin.FetchPluginSites(br.DB, id)
27 |
28 | if len(plugins) <= 0 {
29 | br.NotFoundHandler(w, r)
30 | return
31 | }
32 |
33 | var rows []component.Row
34 | for _, p := range plugins {
35 | comprow := component.Row{
36 | Columns: []component.Column{
37 | {Name: "Name", Value: p.Site.Title},
38 | {Field: component.CheckBoxField{
39 | Name: fmt.Sprintf("%s[]", p.PluginIdentifier),
40 | Identifier: fmt.Sprintf("%s_%d", p.PluginIdentifier, p.Site.ID),
41 | Value: fmt.Sprintf("%d", p.SiteID),
42 | Checked: p.Active,
43 | T: br.Renderer.T,
44 | }},
45 | },
46 | }
47 | rows = append(rows, comprow)
48 | }
49 |
50 | button := component.Button{
51 | Section: "main",
52 | Link: template.URL("/admin/plugins"),
53 | Class: "btn btn-primary",
54 | Content: "Back",
55 | T: br.Renderer.T,
56 | }
57 |
58 | formButton := component.Button{
59 | Section: "main",
60 | Class: "btn btn-primary",
61 | Content: "Save",
62 | T: br.Renderer.T,
63 | }
64 |
65 | table := component.Table{
66 | Section: "main",
67 | Header: []component.Column{
68 | {Name: "Site"},
69 | {Name: "Active"},
70 | },
71 | Rows: rows,
72 | PageNumber: 1,
73 | PageDisplayCount: 10,
74 | T: br.Renderer.T,
75 | }
76 |
77 | form := component.Form{
78 | Section: "main",
79 | Fields: []page.Component{
80 | table,
81 | formButton,
82 | },
83 | T: br.Renderer.T,
84 | Method: "POST",
85 | Action: fmt.Sprintf("/admin/plugins/edit/%s", id),
86 | }
87 |
88 | pageData := structs.PageData{
89 | Template: "admin.page",
90 | Title: "Admin - Edit Plugin",
91 | Components: []page.Component{
92 | button,
93 | form,
94 | },
95 | Themes: br.Renderer.GetThemes(),
96 | }
97 |
98 | br.Renderer.RenderHTMLPage(w, r, pageData)
99 | }
100 |
101 | // AdminPluginEditPost handles editing of plugins
102 | func (br *BeuboRouter) AdminPluginEditPost(w http.ResponseWriter, r *http.Request) {
103 | if !middleware.CanAccess(br.DB, "manage_plugins", r) {
104 | w.WriteHeader(http.StatusForbidden)
105 | return
106 | }
107 |
108 | err := r.ParseForm()
109 | utility.ErrorHandler(err, false)
110 |
111 | params := mux.Vars(r)
112 | id := params["id"]
113 |
114 | plugins := plugin.FetchPluginSites(br.DB, id)
115 |
116 | path := fmt.Sprintf("/admin/plugins/edit/%s", id)
117 |
118 | successMessage := "Plugin updated"
119 |
120 | for key, values := range r.PostForm {
121 | if key == fmt.Sprintf("%s[]", id) {
122 | for _, p := range plugins {
123 | found := false
124 | for _, v := range values {
125 | if v == fmt.Sprintf("%d", p.SiteID) {
126 | found = true
127 | }
128 | }
129 | if found {
130 | // The plugin is active
131 | p.Active = true
132 | } else {
133 | // The plugin is inactive
134 | p.Active = false
135 | }
136 | br.DB.Save(&p)
137 | }
138 | }
139 | }
140 |
141 | // If all sites are set to inactive then we don't get any post data
142 | if len(r.PostForm) <= 0 {
143 | for _, p := range plugins {
144 | p.Active = false
145 | br.DB.Save(&p)
146 | }
147 | }
148 |
149 | utility.SetFlash(w, "message", []byte(successMessage))
150 | http.Redirect(w, r, path, 302)
151 | }
152 |
--------------------------------------------------------------------------------
/pkg/routes/router.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/plugin"
5 | "github.com/uberswe/beubo/pkg/template"
6 | "gorm.io/gorm"
7 | )
8 |
9 | // BeuboRouter holds parameters relevant to the router
10 | type BeuboRouter struct {
11 | DB *gorm.DB
12 | Renderer *template.BeuboTemplateRenderer
13 | PluginHandler *plugin.Handler
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/routes/setting.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "fmt"
5 | "github.com/gorilla/mux"
6 | "github.com/uberswe/beubo/pkg/middleware"
7 | "github.com/uberswe/beubo/pkg/structs"
8 | "github.com/uberswe/beubo/pkg/utility"
9 | "net/http"
10 | "strconv"
11 | )
12 |
13 | // AdminSettingAdd is the route for adding a site
14 | func (br *BeuboRouter) AdminSettingAdd(w http.ResponseWriter, r *http.Request) {
15 | if !middleware.CanAccess(br.DB, "manage_settings", r) {
16 | w.WriteHeader(http.StatusForbidden)
17 | return
18 | }
19 |
20 | pageData := structs.PageData{
21 | Template: "admin.setting.add",
22 | Title: "Admin - Add Setting",
23 | Themes: br.Renderer.GetThemes(),
24 | }
25 |
26 | br.Renderer.RenderHTMLPage(w, r, pageData)
27 | }
28 |
29 | // AdminSettingAddPost handles adding of a global setting
30 | func (br *BeuboRouter) AdminSettingAddPost(w http.ResponseWriter, r *http.Request) {
31 | if !middleware.CanAccess(br.DB, "manage_settings", r) {
32 | w.WriteHeader(http.StatusForbidden)
33 | return
34 | }
35 |
36 | path := "/admin/settings/add"
37 |
38 | successMessage := "Setting created"
39 | invalidError := "an error occurred and the setting could not be created."
40 |
41 | key := r.FormValue("keyField")
42 | value := r.FormValue("valueField")
43 |
44 | if len(key) < 1 {
45 | invalidError = "The key is too short"
46 | utility.SetFlash(w, "error", []byte(invalidError))
47 | http.Redirect(w, r, path, 302)
48 | return
49 | }
50 | if len(value) < 1 {
51 | invalidError = "The value is too short"
52 | utility.SetFlash(w, "error", []byte(invalidError))
53 | http.Redirect(w, r, path, 302)
54 | return
55 | }
56 |
57 | if structs.CreateSetting(br.DB, key, value) {
58 | utility.SetFlash(w, "message", []byte(successMessage))
59 | http.Redirect(w, r, "/admin/settings", 302)
60 | return
61 | }
62 |
63 | utility.SetFlash(w, "error", []byte(invalidError))
64 | http.Redirect(w, r, "/admin/settings/add", 302)
65 | }
66 |
67 | // AdminSettingDelete handles the deletion of a global setting
68 | func (br *BeuboRouter) AdminSettingDelete(w http.ResponseWriter, r *http.Request) {
69 | if !middleware.CanAccess(br.DB, "manage_settings", r) {
70 | w.WriteHeader(http.StatusForbidden)
71 | return
72 | }
73 |
74 | params := mux.Vars(r)
75 | id := params["id"]
76 |
77 | i, err := strconv.Atoi(id)
78 |
79 | utility.ErrorHandler(err, false)
80 |
81 | structs.DeleteSetting(br.DB, i)
82 |
83 | utility.SetFlash(w, "message", []byte("Setting deleted"))
84 |
85 | http.Redirect(w, r, "/admin/settings", 302)
86 | }
87 |
88 | // AdminSettingEdit is the route for adding a setting
89 | func (br *BeuboRouter) AdminSettingEdit(w http.ResponseWriter, r *http.Request) {
90 | if !middleware.CanAccess(br.DB, "manage_settings", r) {
91 | w.WriteHeader(http.StatusForbidden)
92 | return
93 | }
94 |
95 | params := mux.Vars(r)
96 | id := params["id"]
97 |
98 | i, err := strconv.Atoi(id)
99 |
100 | utility.ErrorHandler(err, false)
101 |
102 | setting := structs.FetchSetting(br.DB, i)
103 |
104 | if setting.ID == 0 {
105 | br.NotFoundHandler(w, r)
106 | return
107 | }
108 |
109 | pageData := structs.PageData{
110 | Template: "admin.setting.edit",
111 | Title: "Admin - Edit Site",
112 | Extra: setting,
113 | Themes: br.Renderer.GetThemes(),
114 | }
115 |
116 | br.Renderer.RenderHTMLPage(w, r, pageData)
117 | }
118 |
119 | // AdminSettingEditPost handles editing of a global setting
120 | func (br *BeuboRouter) AdminSettingEditPost(w http.ResponseWriter, r *http.Request) {
121 | if !middleware.CanAccess(br.DB, "manage_settings", r) {
122 | w.WriteHeader(http.StatusForbidden)
123 | return
124 | }
125 |
126 | params := mux.Vars(r)
127 | id := params["id"]
128 |
129 | path := fmt.Sprintf("/admin/settings/edit/%s", id)
130 |
131 | i, err := strconv.Atoi(id)
132 |
133 | utility.ErrorHandler(err, false)
134 |
135 | successMessage := "Setting updated"
136 | invalidError := "an error occurred and the setting could not be updated."
137 |
138 | key := r.FormValue("keyField")
139 | value := r.FormValue("valueField")
140 |
141 | // TODO make rules for models
142 | if len(key) < 1 {
143 | invalidError = "The key is too short"
144 | utility.SetFlash(w, "error", []byte(invalidError))
145 | http.Redirect(w, r, path, 302)
146 | return
147 | }
148 | if len(value) < 1 {
149 | invalidError = "The value is too short"
150 | utility.SetFlash(w, "error", []byte(invalidError))
151 | http.Redirect(w, r, path, 302)
152 | return
153 | }
154 |
155 | if structs.UpdateSetting(br.DB, i, key, value) {
156 | utility.SetFlash(w, "message", []byte(successMessage))
157 | http.Redirect(w, r, "/admin/settings", 302)
158 | return
159 | }
160 |
161 | utility.SetFlash(w, "error", []byte(invalidError))
162 | http.Redirect(w, r, path, 302)
163 | }
164 |
--------------------------------------------------------------------------------
/pkg/routes/site.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "fmt"
5 | "github.com/gorilla/mux"
6 | "github.com/uberswe/beubo/pkg/middleware"
7 | "github.com/uberswe/beubo/pkg/structs"
8 | "github.com/uberswe/beubo/pkg/utility"
9 | "html"
10 | "net/http"
11 | "strconv"
12 | "strings"
13 | )
14 |
15 | // SiteAdmin is the main page for the admin area and shows a list of pages
16 | func (br *BeuboRouter) SiteAdmin(w http.ResponseWriter, r *http.Request) {
17 | if !middleware.CanAccess(br.DB, "manage_pages", r) {
18 | w.WriteHeader(http.StatusForbidden)
19 | return
20 | }
21 |
22 | params := mux.Vars(r)
23 | id := params["id"]
24 |
25 | i, err := strconv.Atoi(id)
26 |
27 | utility.ErrorHandler(err, false)
28 |
29 | site := structs.FetchSite(br.DB, i)
30 |
31 | self := r.Context().Value(middleware.UserContextKey)
32 | if self != nil && self.(structs.User).ID > 0 {
33 | if !self.(structs.User).CanAccessSite(br.DB, site) {
34 | w.WriteHeader(http.StatusForbidden)
35 | return
36 | }
37 | }
38 |
39 | var pages []structs.Page
40 |
41 | extra := make(map[string]interface{})
42 | pagesRes := make(map[string]map[string]string)
43 | extra["SiteID"] = fmt.Sprintf("%d", site.ID)
44 |
45 | if err := br.DB.Where("site_id = ?", site.ID).Find(&pages).Error; err != nil {
46 | utility.ErrorHandler(err, false)
47 | }
48 |
49 | for _, page := range pages {
50 | pid := fmt.Sprintf("%d", page.ID)
51 | pagesRes[pid] = make(map[string]string)
52 | pagesRes[pid]["id"] = pid
53 | pagesRes[pid]["title"] = html.EscapeString(page.Title)
54 | pagesRes[pid]["slug"] = html.EscapeString(page.Slug)
55 | }
56 | extra["pagesRes"] = pagesRes
57 |
58 | pageData := structs.PageData{
59 | Template: "admin.site.page.home",
60 | Title: "Admin",
61 | Extra: extra,
62 | }
63 |
64 | br.Renderer.RenderHTMLPage(w, r, pageData)
65 | }
66 |
67 | // AdminSiteAdd is the route for adding a site
68 | func (br *BeuboRouter) AdminSiteAdd(w http.ResponseWriter, r *http.Request) {
69 | if !middleware.CanAccess(br.DB, "manage_sites", r) {
70 | w.WriteHeader(http.StatusForbidden)
71 | return
72 | }
73 |
74 | pageData := structs.PageData{
75 | Template: "admin.site.add",
76 | Title: "Admin - Add Site",
77 | Themes: br.Renderer.GetThemes(),
78 | }
79 |
80 | br.Renderer.RenderHTMLPage(w, r, pageData)
81 | }
82 |
83 | // AdminSiteAddPost handles the post request for adding a site
84 | func (br *BeuboRouter) AdminSiteAddPost(w http.ResponseWriter, r *http.Request) {
85 | if !middleware.CanAccess(br.DB, "manage_sites", r) {
86 | w.WriteHeader(http.StatusForbidden)
87 | return
88 | }
89 |
90 | path := "/admin/sites/add"
91 |
92 | successMessage := "Site created"
93 | invalidError := "an error occurred and the site could not be created."
94 |
95 | themeID := 0
96 | title := r.FormValue("titleField")
97 | domain := r.FormValue("domainField")
98 | destDomain := r.FormValue("destinationField")
99 | // typeField
100 | // 1 - Beubo hosted site
101 | // 2 - HTML files from directory
102 | // 3 - redirect to a different domain
103 | siteType := r.FormValue("typeField")
104 |
105 | typeID, err := strconv.Atoi(siteType)
106 |
107 | utility.ErrorHandler(err, false)
108 | // Theme is only relevant for Beubo hosted sites
109 | if siteType == "1" {
110 | theme := r.FormValue("themeField")
111 | themeStruct := structs.FetchThemeBySlug(br.DB, theme)
112 | if themeStruct.ID == 0 {
113 | invalidError = "The theme is invalid"
114 | utility.SetFlash(w, "error", []byte(invalidError))
115 | http.Redirect(w, r, path, 302)
116 | return
117 | }
118 | themeID = int(themeStruct.ID)
119 | }
120 |
121 | domain = strings.ToLower(domain)
122 | domain = utility.TrimWhitespace(domain)
123 |
124 | destDomain = strings.ToLower(destDomain)
125 | destDomain = utility.TrimWhitespace(destDomain)
126 |
127 | if len(title) < 1 {
128 | invalidError = "The title is too short"
129 | utility.SetFlash(w, "error", []byte(invalidError))
130 | http.Redirect(w, r, path, 302)
131 | return
132 | }
133 | if len(domain) < 1 {
134 | invalidError = "The domain is too short"
135 | utility.SetFlash(w, "error", []byte(invalidError))
136 | http.Redirect(w, r, path, 302)
137 | return
138 | }
139 |
140 | if site := structs.CreateSite(br.DB, title, domain, typeID, themeID, destDomain); site.ID > 0 {
141 | self := r.Context().Value(middleware.UserContextKey)
142 | if self != nil && self.(structs.User).ID > 0 {
143 | selfUser := self.(structs.User)
144 | site.Users = []*structs.User{
145 | &selfUser,
146 | }
147 | br.DB.Save(&site)
148 | }
149 |
150 | utility.SetFlash(w, "message", []byte(successMessage))
151 | http.Redirect(w, r, "/admin/", 302)
152 | return
153 | }
154 |
155 | utility.SetFlash(w, "error", []byte(invalidError))
156 | http.Redirect(w, r, "/admin/sites/add", 302)
157 | }
158 |
159 | // AdminSiteDelete is the route for deleting a site
160 | func (br *BeuboRouter) AdminSiteDelete(w http.ResponseWriter, r *http.Request) {
161 | if !middleware.CanAccess(br.DB, "manage_sites", r) {
162 | w.WriteHeader(http.StatusForbidden)
163 | return
164 | }
165 |
166 | params := mux.Vars(r)
167 | id := params["id"]
168 | i, err := strconv.Atoi(id)
169 | utility.ErrorHandler(err, false)
170 | site := structs.FetchSite(br.DB, i)
171 | self := r.Context().Value(middleware.UserContextKey)
172 | if self != nil && self.(structs.User).ID > 0 {
173 | if !self.(structs.User).CanAccessSite(br.DB, site) {
174 | w.WriteHeader(http.StatusForbidden)
175 | return
176 | }
177 | }
178 |
179 | structs.DeleteSite(br.DB, i)
180 |
181 | utility.SetFlash(w, "message", []byte("Site deleted"))
182 |
183 | http.Redirect(w, r, "/admin/", 302)
184 | }
185 |
186 | // AdminSiteEdit is the route for adding a site
187 | func (br *BeuboRouter) AdminSiteEdit(w http.ResponseWriter, r *http.Request) {
188 | if !middleware.CanAccess(br.DB, "manage_sites", r) {
189 | w.WriteHeader(http.StatusForbidden)
190 | return
191 | }
192 |
193 | params := mux.Vars(r)
194 | id := params["id"]
195 |
196 | i, err := strconv.Atoi(id)
197 |
198 | utility.ErrorHandler(err, false)
199 |
200 | site := structs.FetchSite(br.DB, i)
201 |
202 | if site.ID == 0 {
203 | br.NotFoundHandler(w, r)
204 | return
205 | }
206 |
207 | self := r.Context().Value(middleware.UserContextKey)
208 | if self != nil && self.(structs.User).ID > 0 {
209 | if !self.(structs.User).CanAccessSite(br.DB, site) {
210 | w.WriteHeader(http.StatusForbidden)
211 | return
212 | }
213 | }
214 |
215 | pageData := structs.PageData{
216 | Template: "admin.site.edit",
217 | Title: "Admin - Edit Site",
218 | Extra: site,
219 | Themes: br.Renderer.GetThemes(),
220 | }
221 |
222 | br.Renderer.RenderHTMLPage(w, r, pageData)
223 | }
224 |
225 | // AdminSiteEditPost handles editing of a site
226 | func (br *BeuboRouter) AdminSiteEditPost(w http.ResponseWriter, r *http.Request) {
227 | if !middleware.CanAccess(br.DB, "manage_sites", r) {
228 | w.WriteHeader(http.StatusForbidden)
229 | return
230 | }
231 |
232 | params := mux.Vars(r)
233 | id := params["id"]
234 |
235 | path := fmt.Sprintf("/admin/sites/edit/%s", id)
236 |
237 | i, err := strconv.Atoi(id)
238 |
239 | utility.ErrorHandler(err, false)
240 | site := structs.FetchSite(br.DB, i)
241 | self := r.Context().Value(middleware.UserContextKey)
242 | if self != nil && self.(structs.User).ID > 0 {
243 | if !self.(structs.User).CanAccessSite(br.DB, site) {
244 | w.WriteHeader(http.StatusForbidden)
245 | return
246 | }
247 | }
248 |
249 | successMessage := "Site updated"
250 | invalidError := "an error occurred and the site could not be updated."
251 |
252 | themeID := 0
253 | title := r.FormValue("titleField")
254 | domain := r.FormValue("domainField")
255 | destDomain := r.FormValue("destinationField")
256 | // typeField
257 | // 1 - Beubo hosted site
258 | // 2 - HTML files from directory
259 | // 3 - redirect to a different domain
260 | siteType := r.FormValue("typeField")
261 |
262 | typeID, err := strconv.Atoi(siteType)
263 |
264 | utility.ErrorHandler(err, false)
265 | // Theme is only relevant for Beubo hosted sites
266 | if siteType == "1" {
267 | theme := r.FormValue("themeField")
268 | themeStruct := structs.FetchThemeBySlug(br.DB, theme)
269 | if themeStruct.ID == 0 {
270 | invalidError = "The theme is invalid"
271 | utility.SetFlash(w, "error", []byte(invalidError))
272 | http.Redirect(w, r, path, 302)
273 | return
274 | }
275 | themeID = int(themeStruct.ID)
276 | }
277 |
278 | domain = strings.ToLower(domain)
279 | domain = utility.TrimWhitespace(domain)
280 |
281 | destDomain = strings.ToLower(destDomain)
282 | destDomain = utility.TrimWhitespace(destDomain)
283 |
284 | if len(title) < 1 {
285 | invalidError = "The title is too short"
286 | utility.SetFlash(w, "error", []byte(invalidError))
287 | http.Redirect(w, r, path, 302)
288 | return
289 | }
290 | if len(domain) < 1 {
291 | invalidError = "The domain is too short"
292 | utility.SetFlash(w, "error", []byte(invalidError))
293 | http.Redirect(w, r, path, 302)
294 | return
295 | }
296 |
297 | if structs.UpdateSite(br.DB, i, title, domain, typeID, themeID, destDomain) {
298 | utility.SetFlash(w, "message", []byte(successMessage))
299 | http.Redirect(w, r, path, 302)
300 | return
301 | }
302 |
303 | utility.SetFlash(w, "error", []byte(invalidError))
304 | http.Redirect(w, r, path, 302)
305 | }
306 |
--------------------------------------------------------------------------------
/pkg/routes/user.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "fmt"
5 | "github.com/gorilla/mux"
6 | "github.com/uberswe/beubo/pkg/middleware"
7 | "github.com/uberswe/beubo/pkg/structs"
8 | "github.com/uberswe/beubo/pkg/structs/page"
9 | "github.com/uberswe/beubo/pkg/structs/page/component"
10 | "github.com/uberswe/beubo/pkg/utility"
11 | "html"
12 | "html/template"
13 | "net/http"
14 | "strconv"
15 | )
16 |
17 | type userAddData struct {
18 | Roles []structs.Role
19 | Sites []structs.Site
20 | }
21 |
22 | // AdminUserAdd is the route for adding a user
23 | func (br *BeuboRouter) AdminUserAdd(w http.ResponseWriter, r *http.Request) {
24 | if !middleware.CanAccess(br.DB, "manage_users", r) {
25 | w.WriteHeader(http.StatusForbidden)
26 | return
27 | }
28 |
29 | roles := []structs.Role{}
30 | sites := []structs.Site{}
31 | br.DB.Find(&roles)
32 | br.DB.Find(&sites)
33 | pageData := structs.PageData{
34 | Template: "admin.user.add",
35 | Title: "Admin - Add User",
36 | Themes: br.Renderer.GetThemes(),
37 | Extra: userAddData{
38 | Roles: roles,
39 | Sites: sites,
40 | },
41 | }
42 |
43 | br.Renderer.RenderHTMLPage(w, r, pageData)
44 | }
45 |
46 | // AdminUserAddPost handles adding of a user
47 | func (br *BeuboRouter) AdminUserAddPost(w http.ResponseWriter, r *http.Request) {
48 | if !middleware.CanAccess(br.DB, "manage_users", r) {
49 | w.WriteHeader(http.StatusForbidden)
50 | return
51 | }
52 |
53 | path := "/admin/users/add"
54 |
55 | successMessage := "User created"
56 | invalidError := "an error occurred and the user could not be created."
57 |
58 | email := r.FormValue("emailField")
59 | password := r.FormValue("passwordField")
60 |
61 | rs := []int{}
62 | ss := []int{}
63 |
64 | for key, values := range r.PostForm {
65 | if key == fmt.Sprintf("%s[]", "roleField") {
66 | for _, value := range values {
67 | valueInt, err := strconv.Atoi(value)
68 | utility.ErrorHandler(err, false)
69 | rs = append(rs, valueInt)
70 | }
71 | } else if key == fmt.Sprintf("%s[]", "siteField") {
72 | for _, value := range values {
73 | valueInt, err := strconv.Atoi(value)
74 | utility.ErrorHandler(err, false)
75 | ss = append(ss, valueInt)
76 | }
77 | }
78 | }
79 |
80 | roles := []*structs.Role{}
81 | if len(rs) > 0 {
82 | br.DB.Find(&roles, rs)
83 | }
84 | sites := []*structs.Site{}
85 | if len(ss) > 0 {
86 | br.DB.Find(&sites, ss)
87 | }
88 |
89 | if !utility.IsEmailValid(email) {
90 | invalidError = "The email is invalid"
91 | utility.SetFlash(w, "error", []byte(invalidError))
92 | http.Redirect(w, r, path, 302)
93 | return
94 | }
95 |
96 | if len(password) < 8 {
97 | invalidError = "The password is too short"
98 | utility.SetFlash(w, "error", []byte(invalidError))
99 | http.Redirect(w, r, path, 302)
100 | return
101 | }
102 |
103 | if structs.CreateUser(br.DB, email, password, roles, sites) {
104 | utility.SetFlash(w, "message", []byte(successMessage))
105 | http.Redirect(w, r, "/admin/users", 302)
106 | return
107 | }
108 |
109 | utility.SetFlash(w, "error", []byte(invalidError))
110 | http.Redirect(w, r, "/admin/users/add", 302)
111 | }
112 |
113 | // AdminUserDelete handles the deletion of a user
114 | func (br *BeuboRouter) AdminUserDelete(w http.ResponseWriter, r *http.Request) {
115 | if !middleware.CanAccess(br.DB, "manage_users", r) {
116 | w.WriteHeader(http.StatusForbidden)
117 | return
118 | }
119 |
120 | params := mux.Vars(r)
121 | id := params["id"]
122 |
123 | i, err := strconv.Atoi(id)
124 |
125 | utility.ErrorHandler(err, false)
126 |
127 | structs.DeleteUser(br.DB, i)
128 |
129 | utility.SetFlash(w, "message", []byte("User deleted"))
130 |
131 | http.Redirect(w, r, "/admin/users", 302)
132 | }
133 |
134 | type userEdit struct {
135 | User structs.User
136 | Role structs.Role
137 | Roles []userEditRole
138 | Features []userEditFeature
139 | Sites []userEditSite
140 | }
141 |
142 | type userEditRole struct {
143 | Role structs.Role
144 | Checked bool
145 | }
146 |
147 | type userEditSite struct {
148 | Site structs.Site
149 | Checked bool
150 | }
151 |
152 | // AdminUserEdit is the route for adding a user
153 | func (br *BeuboRouter) AdminUserEdit(w http.ResponseWriter, r *http.Request) {
154 | if !middleware.CanAccess(br.DB, "manage_users", r) {
155 | w.WriteHeader(http.StatusForbidden)
156 | return
157 | }
158 |
159 | params := mux.Vars(r)
160 | id := params["id"]
161 |
162 | i, err := strconv.Atoi(id)
163 |
164 | utility.ErrorHandler(err, false)
165 |
166 | user := structs.FetchUser(br.DB, i)
167 |
168 | if user.ID == 0 {
169 | br.NotFoundHandler(w, r)
170 | return
171 | }
172 |
173 | ueRoles := []userEditRole{}
174 | roles := []structs.Role{}
175 | br.DB.Find(&roles)
176 | for _, role := range roles {
177 | ueRoles = append(ueRoles, userEditRole{Role: role, Checked: user.HasRole(br.DB, role)})
178 | }
179 |
180 | ueSites := []userEditSite{}
181 | sites := []structs.Site{}
182 | br.DB.Find(&sites)
183 | for _, site := range sites {
184 | ueSites = append(ueSites, userEditSite{Site: site, Checked: user.CanAccessSite(br.DB, site)})
185 | }
186 |
187 | pageData := structs.PageData{
188 | Template: "admin.user.edit",
189 | Title: "Admin - Edit Site",
190 | Extra: userEdit{
191 | User: user,
192 | Roles: ueRoles,
193 | Sites: ueSites,
194 | },
195 | Themes: br.Renderer.GetThemes(),
196 | }
197 |
198 | br.Renderer.RenderHTMLPage(w, r, pageData)
199 | }
200 |
201 | // AdminUserEditPost handles editing of a user
202 | func (br *BeuboRouter) AdminUserEditPost(w http.ResponseWriter, r *http.Request) {
203 | if !middleware.CanAccess(br.DB, "manage_users", r) {
204 | w.WriteHeader(http.StatusForbidden)
205 | return
206 | }
207 |
208 | params := mux.Vars(r)
209 | id := params["id"]
210 |
211 | path := fmt.Sprintf("/admin/users/edit/%s", id)
212 |
213 | i, err := strconv.Atoi(id)
214 |
215 | utility.ErrorHandler(err, false)
216 |
217 | successMessage := "User updated"
218 | invalidError := "an error occurred and the user could not be updated."
219 |
220 | // TODO make rules for models
221 | email := r.FormValue("emailField")
222 | password := r.FormValue("passwordField")
223 |
224 | rs := []int{}
225 | ss := []int{}
226 |
227 | for key, values := range r.PostForm {
228 | if key == fmt.Sprintf("%s[]", "roleField") {
229 | for _, value := range values {
230 | valueInt, err := strconv.Atoi(value)
231 | utility.ErrorHandler(err, false)
232 | rs = append(rs, valueInt)
233 | }
234 | } else if key == fmt.Sprintf("%s[]", "siteField") {
235 | for _, value := range values {
236 | valueInt, err := strconv.Atoi(value)
237 | utility.ErrorHandler(err, false)
238 | ss = append(ss, valueInt)
239 | }
240 | }
241 | }
242 |
243 | roles := []*structs.Role{}
244 | if len(rs) > 0 {
245 | br.DB.Find(&roles, rs)
246 | }
247 | sites := []*structs.Site{}
248 | if len(ss) > 0 {
249 | br.DB.Find(&sites, ss)
250 | }
251 |
252 | if !utility.IsEmailValid(email) {
253 | invalidError = "The email is invalid"
254 | utility.SetFlash(w, "error", []byte(invalidError))
255 | http.Redirect(w, r, path, 302)
256 | return
257 | }
258 | if len(password) > 0 && len(password) < 8 {
259 | invalidError = "The password is too short"
260 | utility.SetFlash(w, "error", []byte(invalidError))
261 | http.Redirect(w, r, path, 302)
262 | return
263 | }
264 |
265 | if structs.UpdateUser(br.DB, i, email, password, roles, sites) {
266 | utility.SetFlash(w, "message", []byte(successMessage))
267 | http.Redirect(w, r, "/admin/users", 302)
268 | return
269 | }
270 |
271 | utility.SetFlash(w, "error", []byte(invalidError))
272 | http.Redirect(w, r, path, 302)
273 | }
274 |
275 | // AdminUserRoles is the route for managing user roles
276 | func (br *BeuboRouter) AdminUserRoles(w http.ResponseWriter, r *http.Request) {
277 | if !middleware.CanAccess(br.DB, "manage_users", r) {
278 | w.WriteHeader(http.StatusForbidden)
279 | return
280 | }
281 |
282 | var roles []structs.Role
283 |
284 | if err := br.DB.Find(&roles).Error; err != nil {
285 | utility.ErrorHandler(err, false)
286 | }
287 |
288 | var rows []component.Row
289 | for _, role := range roles {
290 | sid := fmt.Sprintf("%d", role.ID)
291 | // TODO For now it's not possible to edit default roles. This is because they would be added again when launching Beubo and this needs to be handled in a better way.
292 | if role.Name == "Administrator" || role.Name == "Member" {
293 | rows = append(rows, component.Row{
294 | Columns: []component.Column{
295 | {Name: "ID", Value: sid},
296 | {Name: "Name", Value: html.EscapeString(role.Name)},
297 | {},
298 | {},
299 | },
300 | })
301 | } else {
302 | rows = append(rows, component.Row{
303 | Columns: []component.Column{
304 | {Name: "ID", Value: sid},
305 | {Name: "Name", Value: html.EscapeString(role.Name)},
306 | {Name: "", Field: component.Button{
307 | Link: template.URL(fmt.Sprintf("/admin/users/roles/edit/%s", sid)),
308 | Class: "btn btn-primary",
309 | Content: "Edit",
310 | T: br.Renderer.T,
311 | }},
312 | {Name: "", Field: component.Button{
313 | Link: template.URL(fmt.Sprintf("/admin/users/roles/delete/%s", sid)),
314 | Class: "btn btn-primary",
315 | Content: "Delete",
316 | T: br.Renderer.T,
317 | }},
318 | },
319 | })
320 | }
321 | }
322 |
323 | table := component.Table{
324 | Section: "main",
325 | Header: []component.Column{
326 | {Name: "ID"},
327 | {Name: "Name"},
328 | {Name: ""},
329 | {Name: ""},
330 | },
331 | Rows: rows,
332 | PageNumber: 1,
333 | PageDisplayCount: 10,
334 | T: br.Renderer.T,
335 | }
336 |
337 | pageData := structs.PageData{
338 | Template: "admin.page",
339 | Title: "Admin - User Roles",
340 | Components: []page.Component{
341 | component.Button{
342 | Section: "main",
343 | Link: template.URL("/admin/users/roles/add"),
344 | Class: "btn btn-primary",
345 | Content: "Add Role",
346 | T: br.Renderer.T,
347 | },
348 | table,
349 | },
350 | }
351 |
352 | br.Renderer.RenderHTMLPage(w, r, pageData)
353 | }
354 |
355 | // AdminUserRoleAdd is the route for adding a user role
356 | func (br *BeuboRouter) AdminUserRoleAdd(w http.ResponseWriter, r *http.Request) {
357 | if !middleware.CanAccess(br.DB, "manage_user_roles", r) {
358 | w.WriteHeader(http.StatusForbidden)
359 | return
360 | }
361 |
362 | features := []structs.Feature{}
363 | br.DB.Find(&features)
364 |
365 | pageData := structs.PageData{
366 | Template: "admin.user.role.add",
367 | Title: "Admin - Add Role",
368 | Themes: br.Renderer.GetThemes(),
369 | Extra: features,
370 | }
371 |
372 | br.Renderer.RenderHTMLPage(w, r, pageData)
373 | }
374 |
375 | // AdminUserRoleAddPost handles adding of a user role
376 | func (br *BeuboRouter) AdminUserRoleAddPost(w http.ResponseWriter, r *http.Request) {
377 | if !middleware.CanAccess(br.DB, "manage_user_roles", r) {
378 | w.WriteHeader(http.StatusForbidden)
379 | return
380 | }
381 |
382 | path := "/admin/users/roles/add"
383 |
384 | successMessage := "Role created"
385 | invalidError := "an error occurred and the user could not be created."
386 |
387 | name := r.FormValue("nameField")
388 |
389 | if len(name) < 2 {
390 | invalidError = "The name is too short"
391 | utility.SetFlash(w, "error", []byte(invalidError))
392 | http.Redirect(w, r, path, 302)
393 | return
394 | }
395 |
396 | if structs.CreateRole(br.DB, name) {
397 | utility.SetFlash(w, "message", []byte(successMessage))
398 | http.Redirect(w, r, "/admin/users/roles", 302)
399 | return
400 | }
401 |
402 | utility.SetFlash(w, "error", []byte(invalidError))
403 | http.Redirect(w, r, "/admin/users/roles/add", 302)
404 | }
405 |
406 | // AdminUserRoleDelete handles the deletion of a user role
407 | func (br *BeuboRouter) AdminUserRoleDelete(w http.ResponseWriter, r *http.Request) {
408 | if !middleware.CanAccess(br.DB, "manage_user_roles", r) {
409 | w.WriteHeader(http.StatusForbidden)
410 | return
411 | }
412 |
413 | params := mux.Vars(r)
414 | id := params["id"]
415 |
416 | i, err := strconv.Atoi(id)
417 |
418 | utility.ErrorHandler(err, false)
419 |
420 | role := structs.FetchRole(br.DB, i)
421 | if role.ID != 0 && !role.IsDefault() {
422 | structs.DeleteRole(br.DB, i)
423 | } else {
424 | utility.SetFlash(w, "error", []byte("Can not delete a default role"))
425 | http.Redirect(w, r, "/admin/users/roles", 302)
426 | return
427 | }
428 |
429 | utility.SetFlash(w, "message", []byte("Role deleted"))
430 |
431 | http.Redirect(w, r, "/admin/users/roles", 302)
432 | }
433 |
434 | type userEditFeature struct {
435 | Feature structs.Feature
436 | Checked bool
437 | }
438 |
439 | // AdminUserRoleEdit is the route for adding a user role
440 | func (br *BeuboRouter) AdminUserRoleEdit(w http.ResponseWriter, r *http.Request) {
441 | if !middleware.CanAccess(br.DB, "manage_user_roles", r) {
442 | w.WriteHeader(http.StatusForbidden)
443 | return
444 | }
445 |
446 | params := mux.Vars(r)
447 | id := params["id"]
448 |
449 | i, err := strconv.Atoi(id)
450 |
451 | utility.ErrorHandler(err, false)
452 |
453 | role := structs.FetchRole(br.DB, i)
454 | if role.ID == 0 || role.IsDefault() {
455 | br.NotFoundHandler(w, r)
456 | return
457 | }
458 |
459 | ueFeatures := []userEditFeature{}
460 | features := []structs.Feature{}
461 | br.DB.Find(&features)
462 | for _, feature := range features {
463 | ueFeatures = append(ueFeatures, userEditFeature{Feature: feature, Checked: role.HasFeature(br.DB, feature)})
464 | }
465 |
466 | pageData := structs.PageData{
467 | Template: "admin.user.role.edit",
468 | Title: "Admin - Edit Role",
469 | Extra: userEdit{Features: ueFeatures, Role: role},
470 | Themes: br.Renderer.GetThemes(),
471 | }
472 |
473 | br.Renderer.RenderHTMLPage(w, r, pageData)
474 | }
475 |
476 | // AdminUserRoleEditPost handles editing of a user role
477 | func (br *BeuboRouter) AdminUserRoleEditPost(w http.ResponseWriter, r *http.Request) {
478 | if !middleware.CanAccess(br.DB, "manage_user_roles", r) {
479 | w.WriteHeader(http.StatusForbidden)
480 | return
481 | }
482 |
483 | err := r.ParseForm()
484 | utility.ErrorHandler(err, false)
485 | params := mux.Vars(r)
486 | id := params["id"]
487 |
488 | path := fmt.Sprintf("/admin/users/roles/edit/%s", id)
489 |
490 | i, err := strconv.Atoi(id)
491 |
492 | utility.ErrorHandler(err, false)
493 |
494 | successMessage := "Role updated"
495 | invalidError := "an error occurred and the role could not be updated."
496 |
497 | fs := []int{}
498 |
499 | name := r.FormValue("nameField")
500 | for key, values := range r.PostForm {
501 | if key == fmt.Sprintf("%s[]", "featureField") {
502 | for _, value := range values {
503 | valueInt, err := strconv.Atoi(value)
504 | utility.ErrorHandler(err, false)
505 | fs = append(fs, valueInt)
506 | }
507 | }
508 | }
509 |
510 | features := []*structs.Feature{}
511 | if len(fs) > 0 {
512 | br.DB.Find(&features, fs)
513 | }
514 |
515 | if len(name) < 2 {
516 | invalidError = "The name is too short"
517 | utility.SetFlash(w, "error", []byte(invalidError))
518 | http.Redirect(w, r, path, 302)
519 | return
520 | }
521 |
522 | if structs.UpdateRole(br.DB, i, name, features) {
523 | utility.SetFlash(w, "message", []byte(successMessage))
524 | http.Redirect(w, r, "/admin/users/roles", 302)
525 | return
526 | }
527 |
528 | utility.SetFlash(w, "error", []byte(invalidError))
529 | http.Redirect(w, r, path, 302)
530 | }
531 |
--------------------------------------------------------------------------------
/pkg/structs/README.md:
--------------------------------------------------------------------------------
1 | ## Models
2 |
3 | This folder should hold all Beubo structs
--------------------------------------------------------------------------------
/pkg/structs/config.go:
--------------------------------------------------------------------------------
1 | package structs
2 |
3 | import "gorm.io/gorm"
4 |
5 | // Config is a key value store for various settings and configurations
6 | type Config struct {
7 | gorm.Model
8 | Key string `gorm:"size:255;unique_index"`
9 | Value string `sql:"type:text"`
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/structs/page.go:
--------------------------------------------------------------------------------
1 | package structs
2 |
3 | import (
4 | "fmt"
5 | "github.com/uberswe/beubo/pkg/structs/page"
6 | "gorm.io/gorm"
7 | "html/template"
8 | )
9 |
10 | // Page represents the content of a page, I wanted to go with the concept of having everything be a post even if it's a page, contact form or product
11 | type Page struct {
12 | gorm.Model
13 | Title string `gorm:"size:255"`
14 | Content string `sql:"type:text"`
15 | Description string `sql:"type:text"`
16 | Excerpt string `sql:"type:text"`
17 | Slug string `gorm:"size:255;unique_index:idx_slug_site_id"`
18 | Template string `gorm:"size:255"`
19 | Site Site
20 | SiteID int `gorm:"unique_index:idx_slug_site_id"`
21 | Tags []Tag `gorm:"many2many:page_tags;"`
22 | }
23 |
24 | // PageData is a general structure that holds all data that can be displayed on a page
25 | // using go html templates
26 | type PageData struct {
27 | Theme string
28 | Template string
29 | Templates map[string]string
30 | Themes map[string]string
31 | Title string
32 | WebsiteName string
33 | URL string
34 | Error string
35 | Warning string
36 | Message string
37 | Year string
38 | Stylesheets []string
39 | Scripts []string
40 | Favicon string
41 | Extra interface{}
42 | Components []page.Component
43 | Menus []page.Menu
44 | }
45 |
46 | // Tag of a post can be used for post categories or things like meta tag keywords for example
47 | type Tag struct {
48 | gorm.Model
49 | Value string `gorm:"unique;not null"`
50 | }
51 |
52 | // JSONTag is used for responses where tags are shown
53 | // TODO is this redundant if we can use Tag?
54 | type JSONTag struct {
55 | Value string `json:"value"`
56 | }
57 |
58 | // Comment can be related to a post created by a user
59 | type Comment struct {
60 | gorm.Model
61 | User User
62 | UserID int
63 | Email string
64 | Website string
65 | Text string
66 | Page Page
67 | PageID int
68 | }
69 |
70 | // CreatePage is a method which creates a page using gorm
71 | func CreatePage(db *gorm.DB, title string, slug string, tags []Tag, template string, content string, siteID int) bool {
72 | pageData := Page{
73 | Title: title,
74 | Content: content,
75 | Slug: slug,
76 | Template: template,
77 | SiteID: siteID,
78 | Tags: tags,
79 | }
80 |
81 | if err := db.Create(&pageData).Error; err != nil {
82 | fmt.Println("Could not create pageData")
83 | return false
84 | }
85 | return true
86 | }
87 |
88 | // FetchPage gets a page based on the provided id from the database
89 | func FetchPage(db *gorm.DB, id int) Page {
90 | pageData := Page{}
91 |
92 | db.Preload("Tags").First(&pageData, id)
93 |
94 | return pageData
95 | }
96 |
97 | // FetchPageBySiteIDAndSlug gets a page based on the site id and slug from the database
98 | func FetchPageBySiteIDAndSlug(db *gorm.DB, SiteID int, slug string) Page {
99 | pageData := Page{}
100 | db.Where("slug = ? AND site_id = ?", slug, SiteID).First(&pageData)
101 | return pageData
102 | }
103 |
104 | // UpdatePage is a method which updates a page in the database with relevant data
105 | func UpdatePage(db *gorm.DB, id int, title string, slug string, tags []Tag, template string, content string, siteID int) bool {
106 | pageData := FetchPage(db, id)
107 |
108 | db.Model(&pageData).Association("Tags").Clear()
109 |
110 | pageData.Title = title
111 | pageData.Slug = slug
112 | pageData.Content = content
113 | pageData.Template = template
114 | pageData.SiteID = siteID
115 | pageData.Tags = tags
116 |
117 | if err := db.Save(&pageData).Error; err != nil {
118 | fmt.Println("Could not create site")
119 | return false
120 | }
121 | return true
122 | }
123 |
124 | // DeletePage deletes a page with the provided id from the database
125 | func DeletePage(db *gorm.DB, id int) Page {
126 | pageData := FetchPage(db, id)
127 | db.Delete(&pageData)
128 | return pageData
129 | }
130 |
131 | // Content renders components for a page for the specified section
132 | func (pd PageData) Content(section string) template.HTML {
133 | result := ""
134 | for _, component := range pd.Components {
135 | if component.GetSection() == section {
136 | result += component.Render()
137 | }
138 | }
139 | return template.HTML(result)
140 | }
141 |
142 | // Menu renders a menu for the provided section
143 | func (pd PageData) Menu(section string) template.HTML {
144 | result := ""
145 | for _, menu := range pd.Menus {
146 | if menu.GetIdentifier() == section {
147 | return template.HTML(menu.Render())
148 | }
149 | }
150 | return template.HTML(result)
151 | }
152 |
--------------------------------------------------------------------------------
/pkg/structs/page/component.go:
--------------------------------------------------------------------------------
1 | package page
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "html/template"
7 | "log"
8 | )
9 |
10 | // Component is anything that can be rendered on a page. A text field is a component but so is the form the text field is a part of.
11 | type Component interface {
12 | GetSection() string
13 | // render returns a html template string with the content of the field
14 | Render() string
15 | GetTemplateName() string
16 | GetTheme() string
17 | GetTemplate() *template.Template
18 | }
19 |
20 | // RenderComponent takes the provided component and finds the relevant template and renders this into a string
21 | func RenderComponent(c Component) string {
22 | path := fmt.Sprintf("%s.%s", c.GetTheme(), c.GetTemplateName())
23 | var foundTemplate *template.Template
24 | if c.GetTemplate() == nil {
25 | return ""
26 | }
27 | if foundTemplate = c.GetTemplate().Lookup(path); foundTemplate == nil {
28 | log.Printf("Component file not found %s\n", path)
29 | return ""
30 | }
31 | buf := &bytes.Buffer{}
32 | err := foundTemplate.Execute(buf, c)
33 | if err != nil {
34 | log.Printf("Component file error executing template %s\n", path)
35 | return ""
36 | }
37 | return buf.String()
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/structs/page/component/button.go:
--------------------------------------------------------------------------------
1 | package component
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs/page"
5 | "html/template"
6 | )
7 |
8 | // Button is a beubo component that can be rendered using HTML templates
9 | type Button struct {
10 | Section string
11 | Theme string
12 | Template string
13 | Class string
14 | Content string
15 | Link template.URL
16 | T *template.Template
17 | }
18 |
19 | // GetSection is a getter for the Section property
20 | func (b Button) GetSection() string {
21 | return b.Section
22 | }
23 |
24 | // GetTemplateName is a getter for the Template Property
25 | func (b Button) GetTemplateName() string {
26 | return returnTIfNotEmpty(b.Template, "component.button")
27 | }
28 |
29 | // GetTheme is a getter for the Theme Property
30 | func (b Button) GetTheme() string {
31 | return returnTIfNotEmpty(b.Theme, "default")
32 | }
33 |
34 | // GetTemplate is a getter for the T Property
35 | func (b Button) GetTemplate() *template.Template {
36 | return b.T
37 | }
38 |
39 | // Render calls RenderComponent to turn a Component into a html string for browser output
40 | func (b Button) Render() string {
41 | return page.RenderComponent(b)
42 | }
43 |
--------------------------------------------------------------------------------
/pkg/structs/page/component/checkboxfield.go:
--------------------------------------------------------------------------------
1 | package component
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs/page"
5 | "html/template"
6 | )
7 |
8 | // CheckBoxField is a beubo component that can be rendered using HTML templates
9 | type CheckBoxField struct {
10 | Theme string
11 | Template string
12 | Content string
13 | Class string
14 | Name string
15 | Identifier string
16 | Value string
17 | Checked bool
18 | T *template.Template
19 | }
20 |
21 | // GetSection is a getter for the Section property
22 | func (cb CheckBoxField) GetSection() string {
23 | return ""
24 | }
25 |
26 | // GetTemplateName is a getter for the Template Property
27 | func (cb CheckBoxField) GetTemplateName() string {
28 | return returnTIfNotEmpty(cb.Template, "component.checkboxfield")
29 | }
30 |
31 | // GetTheme is a getter for the Theme Property
32 | func (cb CheckBoxField) GetTheme() string {
33 | return returnTIfNotEmpty(cb.Theme, "default")
34 | }
35 |
36 | // GetTemplate is a getter for the T Property
37 | func (cb CheckBoxField) GetTemplate() *template.Template {
38 | return cb.T
39 | }
40 |
41 | // Render calls RenderComponent to turn a Component into a html string for browser output
42 | func (cb CheckBoxField) Render() string {
43 | return page.RenderComponent(cb)
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/structs/page/component/form.go:
--------------------------------------------------------------------------------
1 | package component
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs/page"
5 | "html/template"
6 | )
7 |
8 | // Form is a beubo component that can be rendered using HTML templates
9 | type Form struct {
10 | Section string
11 | Fields []page.Component
12 | Theme string
13 | Template string
14 | T *template.Template
15 | Method string
16 | Action string
17 | }
18 |
19 | // GetSection is a getter for the Section property
20 | func (f Form) GetSection() string {
21 | return f.Section
22 | }
23 |
24 | // GetTemplateName is a getter for the Template Property
25 | func (f Form) GetTemplateName() string {
26 | return returnTIfNotEmpty(f.Template, "component.form")
27 | }
28 |
29 | // GetTheme is a getter for the Theme Property
30 | func (f Form) GetTheme() string {
31 | return returnTIfNotEmpty(f.Theme, "default")
32 | }
33 |
34 | // GetTemplate is a getter for the T Property
35 | func (f Form) GetTemplate() *template.Template {
36 | return f.T
37 | }
38 |
39 | // Render calls RenderComponent to turn a Component into a html string for browser output
40 | func (f Form) Render() string {
41 | return page.RenderComponent(f)
42 | }
43 |
44 | // RenderField calls Render to turn a Column into a string which is added to the Form Render
45 | func (f Form) RenderField(value string, field page.Component) template.HTML {
46 | if field != nil && field.Render() != "" {
47 | return template.HTML(field.Render())
48 | }
49 | return template.HTML(value)
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/structs/page/component/functions.go:
--------------------------------------------------------------------------------
1 | package component
2 |
3 | func returnTIfNotEmpty(t string, d string) string {
4 | if t != "" {
5 | return t
6 | }
7 | return d
8 | }
9 |
--------------------------------------------------------------------------------
/pkg/structs/page/component/hiddenfield.go:
--------------------------------------------------------------------------------
1 | package component
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs/page"
5 | "html/template"
6 | )
7 |
8 | // HiddenField is a beubo component that can be rendered using HTML templates
9 | type HiddenField struct {
10 | Theme string
11 | Template string
12 | Identifier string
13 | Name string
14 | Value string
15 | T *template.Template
16 | }
17 |
18 | // GetSection is a getter for the Section property
19 | func (hf HiddenField) GetSection() string {
20 | return ""
21 | }
22 |
23 | // GetTemplateName is a getter for the Template Property
24 | func (hf HiddenField) GetTemplateName() string {
25 | return returnTIfNotEmpty(hf.Template, "component.hiddenfield")
26 | }
27 |
28 | // GetTheme is a getter for the Theme Property
29 | func (hf HiddenField) GetTheme() string {
30 | return returnTIfNotEmpty(hf.Theme, "default")
31 | }
32 |
33 | // GetTemplate is a getter for the T Property
34 | func (hf HiddenField) GetTemplate() *template.Template {
35 | return hf.T
36 | }
37 |
38 | // Render calls RenderComponent to turn a Component into a html string for browser output
39 | func (hf HiddenField) Render() string {
40 | return page.RenderComponent(hf)
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/structs/page/component/radiofield.go:
--------------------------------------------------------------------------------
1 | package component
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs/page"
5 | "html/template"
6 | )
7 |
8 | // RadioField is a beubo component that can be rendered using HTML templates
9 | type RadioField struct {
10 | Theme string
11 | Template string
12 | Class string
13 | Identifier string
14 | Name string
15 | Value string
16 | Content string
17 | Checked bool
18 | T *template.Template
19 | }
20 |
21 | // GetSection is a getter for the Section property
22 | func (rf RadioField) GetSection() string {
23 | return ""
24 | }
25 |
26 | // GetTemplateName is a getter for the Template Property
27 | func (rf RadioField) GetTemplateName() string {
28 | return returnTIfNotEmpty(rf.Template, "component.radiofield")
29 | }
30 |
31 | // GetTheme is a getter for the Theme Property
32 | func (rf RadioField) GetTheme() string {
33 | return returnTIfNotEmpty(rf.Theme, "default")
34 | }
35 |
36 | // GetTemplate is a getter for the T Property
37 | func (rf RadioField) GetTemplate() *template.Template {
38 | return rf.T
39 | }
40 |
41 | // Render calls RenderComponent to turn a Component into a html string for browser output
42 | func (rf RadioField) Render() string {
43 | return page.RenderComponent(rf)
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/structs/page/component/selectfield.go:
--------------------------------------------------------------------------------
1 | package component
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs/page"
5 | "html/template"
6 | )
7 |
8 | // SelectField is a beubo component that can be rendered using HTML templates
9 | type SelectField struct {
10 | Theme string
11 | Template string
12 | Identifier string
13 | Class string
14 | Name string
15 | Options []SelectFieldOption
16 | T *template.Template
17 | }
18 |
19 | // SelectFieldOption is part of the SelectField values, there should be one or more of these
20 | type SelectFieldOption struct {
21 | Value string
22 | Content string
23 | }
24 |
25 | // GetSection is a getter for the Section property
26 | func (sf SelectField) GetSection() string {
27 | return ""
28 | }
29 |
30 | // GetTemplateName is a getter for the Template Property
31 | func (sf SelectField) GetTemplateName() string {
32 | return returnTIfNotEmpty(sf.Template, "component.selectfield")
33 | }
34 |
35 | // GetTheme is a getter for the Theme Property
36 | func (sf SelectField) GetTheme() string {
37 | return returnTIfNotEmpty(sf.Theme, "default")
38 | }
39 |
40 | // GetTemplate is a getter for the T Property
41 | func (sf SelectField) GetTemplate() *template.Template {
42 | return sf.T
43 | }
44 |
45 | // Render calls RenderComponent to turn a Component into a html string for browser output
46 | func (sf SelectField) Render() string {
47 | return page.RenderComponent(sf)
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/structs/page/component/table.go:
--------------------------------------------------------------------------------
1 | package component
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs/page"
5 | "html/template"
6 | )
7 |
8 | // Table is a beubo component that can be rendered using HTML templates
9 | type Table struct {
10 | Section string
11 | Header []Column
12 | Rows []Row
13 | Theme string
14 | Template string
15 | PageNumber int // Current page
16 | PageDisplayCount int // How many rows per page
17 | T *template.Template
18 | }
19 |
20 | // GetSection is a getter for the Section property
21 | func (t Table) GetSection() string {
22 | return t.Section
23 | }
24 |
25 | // Row represents a html table row which can have columns
26 | type Row struct {
27 | Columns []Column
28 | }
29 |
30 | // Column represents a html column in a table which is part of a row
31 | type Column struct {
32 | Name string
33 | Field page.Component
34 | Value string
35 | }
36 |
37 | // GetTemplateName is a getter for the Template Property
38 | func (t Table) GetTemplateName() string {
39 | return returnTIfNotEmpty(t.Template, "component.table")
40 | }
41 |
42 | // GetTheme is a getter for the Theme Property
43 | func (t Table) GetTheme() string {
44 | return returnTIfNotEmpty(t.Theme, "default")
45 | }
46 |
47 | // GetTemplate is a getter for the T Property
48 | func (t Table) GetTemplate() *template.Template {
49 | return t.T
50 | }
51 |
52 | // Render calls RenderComponent to turn a Component into a html string for browser output
53 | func (t Table) Render() string {
54 | return page.RenderComponent(t)
55 | }
56 |
57 | // RenderColumn calls RenderComponent to turn a Column into a html string which is added to the Table Render
58 | func (t Table) RenderColumn(c Column) string {
59 | return page.RenderComponent(c.Field)
60 | }
61 |
62 | // RenderField calls Render to turn a Column into a string which is added to the Table Render
63 | func (c Column) RenderField(value string, field page.Component) template.HTML {
64 | if field != nil && field.Render() != "" {
65 | return template.HTML(field.Render())
66 | }
67 | return template.HTML(value)
68 | }
69 |
--------------------------------------------------------------------------------
/pkg/structs/page/component/text.go:
--------------------------------------------------------------------------------
1 | package component
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs/page"
5 | "html/template"
6 | )
7 |
8 | // Text is a beubo component that can be rendered using HTML templates
9 | type Text struct {
10 | Section string
11 | Content template.HTML
12 | Theme string
13 | Template string
14 | Class string
15 | T *template.Template
16 | }
17 |
18 | // GetSection is a getter for the Section property
19 | func (t Text) GetSection() string {
20 | return t.Section
21 | }
22 |
23 | // GetTemplateName is a getter for the Template property
24 | func (t Text) GetTemplateName() string {
25 | return returnTIfNotEmpty(t.Template, "component.text")
26 | }
27 |
28 | // GetTheme is a getter for the Theme property
29 | func (t Text) GetTheme() string {
30 | return returnTIfNotEmpty(t.Theme, "default")
31 | }
32 |
33 | // GetTemplate is a getter for the T Property
34 | func (t Text) GetTemplate() *template.Template {
35 | return t.T
36 | }
37 |
38 | // Render calls RenderComponent to turn a Component into a html string for browser output
39 | func (t Text) Render() string {
40 | return page.RenderComponent(t)
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/structs/page/component/textareafield.go:
--------------------------------------------------------------------------------
1 | package component
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs/page"
5 | "html/template"
6 | )
7 |
8 | // TextAreaField is a beubo component that can be rendered using HTML templates
9 | type TextAreaField struct {
10 | Content string
11 | Theme string
12 | Template string
13 | Class string
14 | Identifier string
15 | Label string
16 | Name string
17 | Rows int
18 | Cols int
19 | T *template.Template
20 | }
21 |
22 | // GetSection is a getter for the Section property
23 | func (t TextAreaField) GetSection() string {
24 | return ""
25 | }
26 |
27 | // GetTemplateName is a getter for the Template property
28 | func (t TextAreaField) GetTemplateName() string {
29 | return returnTIfNotEmpty(t.Template, "component.textareafield")
30 | }
31 |
32 | // GetTheme is a getter for the Theme property
33 | func (t TextAreaField) GetTheme() string {
34 | return returnTIfNotEmpty(t.Theme, "default")
35 | }
36 |
37 | // GetTemplate is a getter for the T Property
38 | func (t TextAreaField) GetTemplate() *template.Template {
39 | return t.T
40 | }
41 |
42 | // Render calls RenderComponent to turn a Component into a html string for browser output
43 | func (t TextAreaField) Render() string {
44 | return page.RenderComponent(t)
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/structs/page/component/textfield.go:
--------------------------------------------------------------------------------
1 | package component
2 |
3 | import (
4 | "github.com/uberswe/beubo/pkg/structs/page"
5 | "html/template"
6 | )
7 |
8 | // TextField is a beubo component that can be rendered using HTML templates
9 | type TextField struct {
10 | Theme string
11 | Template string
12 | Class string
13 | Identifier string
14 | Label string
15 | Name string
16 | Value string
17 | Placeholder string
18 | T *template.Template
19 | }
20 |
21 | // GetSection is a getter for the Section property
22 | func (t TextField) GetSection() string {
23 | return ""
24 | }
25 |
26 | // GetTemplateName is a getter for the Template property
27 | func (t TextField) GetTemplateName() string {
28 | return returnTIfNotEmpty(t.Template, "component.textfield")
29 | }
30 |
31 | // GetTheme is a getter for the Theme property
32 | func (t TextField) GetTheme() string {
33 | return returnTIfNotEmpty(t.Theme, "default")
34 | }
35 |
36 | // GetTemplate is a getter for the T Property
37 | func (t TextField) GetTemplate() *template.Template {
38 | return t.T
39 | }
40 |
41 | // Render calls RenderComponent to turn a Component into a html string for browser output
42 | func (t TextField) Render() string {
43 | return page.RenderComponent(t)
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/structs/page/menu.go:
--------------------------------------------------------------------------------
1 | package page
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "html/template"
7 | "log"
8 | )
9 |
10 | // Menu is a component but requires a slice of menu items
11 | type Menu interface {
12 | GetIdentifier() string
13 | GetItems() []MenuItem
14 | Render() string
15 | }
16 |
17 | // MenuItem is part of a Menu and usually represents a clickable link
18 | type MenuItem struct {
19 | Text string
20 | URI string
21 | Template string
22 | Theme string
23 | Items []MenuItem // A menu item can contain submenus
24 | T *template.Template
25 | }
26 |
27 | // SubMenu renders submenu items recursively in templates
28 | func (m MenuItem) SubMenu() template.HTML {
29 | if len(m.Items) > 0 {
30 | tmpl := "menu.sub"
31 | if m.Template != "" {
32 | tmpl = m.Template
33 | }
34 | theme := "default"
35 | if m.Theme != "" {
36 | theme = m.Theme
37 | }
38 | path := fmt.Sprintf("%s.%s", theme, tmpl)
39 | var foundTemplate *template.Template
40 | if foundTemplate = m.T.Lookup(path); foundTemplate == nil {
41 | log.Printf("Menu file not found %s\n", path)
42 | return ""
43 | }
44 | buf := &bytes.Buffer{}
45 | err := foundTemplate.Execute(buf, m)
46 | if err != nil {
47 | log.Printf("Component file error executing template %s\n", path)
48 | return ""
49 | }
50 | return template.HTML(buf.String())
51 | }
52 | return ""
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/structs/page/menu/default.go:
--------------------------------------------------------------------------------
1 | package menu
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "github.com/uberswe/beubo/pkg/structs/page"
7 | "html/template"
8 | "log"
9 | )
10 |
11 | // DefaultMenu is the base menu struct for Beubo
12 | type DefaultMenu struct {
13 | Items []page.MenuItem
14 | Identifier string
15 | Template string
16 | Theme string
17 | T *template.Template
18 | }
19 |
20 | // GetIdentifier is a getter for the Identifier property
21 | func (m DefaultMenu) GetIdentifier() string {
22 | return m.Identifier
23 | }
24 |
25 | // GetItems is a getter for the Items property
26 | func (m DefaultMenu) GetItems() []page.MenuItem {
27 | return m.Items
28 | }
29 |
30 | // Render calls the relevant templates and functions to turn a menu into a html string for browser output
31 | func (m DefaultMenu) Render() string {
32 | tmpl := "menu.default"
33 | if m.Template != "" {
34 | tmpl = m.Template
35 | }
36 | theme := "default"
37 | if m.Theme != "" {
38 | theme = m.Theme
39 | }
40 | path := fmt.Sprintf("%s.%s", theme, tmpl)
41 | var foundTemplate *template.Template
42 | if foundTemplate = m.T.Lookup(path); foundTemplate == nil {
43 | log.Printf("Menu file not found %s\n", path)
44 | return ""
45 | }
46 | buf := &bytes.Buffer{}
47 | err := foundTemplate.Execute(buf, m)
48 | if err != nil {
49 | log.Printf("Component file error executing template %s\n", path)
50 | return ""
51 | }
52 | return buf.String()
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/structs/role.go:
--------------------------------------------------------------------------------
1 | package structs
2 |
3 | import (
4 | "fmt"
5 | "github.com/uberswe/beubo/pkg/utility"
6 | "gorm.io/gorm"
7 | )
8 |
9 | // Role has one or more users and one or more features. A user belonging to a role which also has a feature will allow that user to use the feature
10 | type Role struct {
11 | gorm.Model
12 | Name string `gorm:"size:255"`
13 | Users []*User `gorm:"many2many:user_roles;"`
14 | Features []*Feature `gorm:"many2many:role_features;"`
15 | }
16 |
17 | // Feature contains a key referencing features in the application
18 | type Feature struct {
19 | gorm.Model
20 | Key string `gorm:"size:255"`
21 | Roles []*Role `gorm:"many2many:role_features;"`
22 | }
23 |
24 | // IsDefault checks if the role is a default role
25 | // TODO find a better way to handle default roles
26 | func (r Role) IsDefault() bool {
27 | if r.Name == "Administrator" || r.Name == "Member" {
28 | return true
29 | }
30 | return false
31 | }
32 |
33 | // HasFeature checks if a role has the specified feature
34 | func (r Role) HasFeature(db *gorm.DB, f Feature) bool {
35 | features := []Feature{}
36 | _ = db.Model(&r).Where("key = ?", f.Key).Association("Features").Find(&features)
37 | if len(features) >= 1 {
38 | return true
39 | }
40 | return false
41 | }
42 |
43 | // CreateRole is a method which creates a role using gorm
44 | func CreateRole(db *gorm.DB, name string) bool {
45 | checkRole := FetchRoleByName(db, name)
46 | if checkRole.ID != 0 {
47 | return false
48 | }
49 | role := Role{Name: name}
50 |
51 | if err := db.Create(&role).Error; err != nil {
52 | fmt.Println("Could not create role")
53 | return false
54 | }
55 | return true
56 | }
57 |
58 | // FetchRoleByName retrieves a role from the database using the provided name
59 | func FetchRoleByName(db *gorm.DB, name string) Role {
60 | role := Role{}
61 | db.Where("name = ?", name).First(&role)
62 | return role
63 | }
64 |
65 | // FetchRole retrieves a role from the database using the provided id
66 | func FetchRole(db *gorm.DB, id int) Role {
67 | role := Role{}
68 | db.First(&role, id)
69 | return role
70 | }
71 |
72 | // DeleteRole deletes a user by id
73 | func DeleteRole(db *gorm.DB, id int) (role Role) {
74 | db.First(&role, id)
75 | db.Delete(&role)
76 | return role
77 | }
78 |
79 | // UpdateRole updates the role struct with the provided details
80 | func UpdateRole(db *gorm.DB, id int, name string, features []*Feature) bool {
81 | role := FetchRole(db, id)
82 | checkRole := FetchRoleByName(db, name)
83 | if checkRole.ID != 0 && checkRole.ID != role.ID {
84 | return false
85 | }
86 | err := db.Model(&role).Association("Features").Clear()
87 | utility.ErrorHandler(err, false)
88 | role.Name = name
89 | role.Features = features
90 | if err := db.Save(&role).Error; err != nil {
91 | fmt.Println("Could not create role")
92 | return false
93 | }
94 | return true
95 | }
96 |
--------------------------------------------------------------------------------
/pkg/structs/session.go:
--------------------------------------------------------------------------------
1 | package structs
2 |
3 | import (
4 | "fmt"
5 | "github.com/uberswe/beubo/pkg/utility"
6 | "gorm.io/gorm"
7 | )
8 |
9 | // Session represents an authenticated user session, there can be multiple sessions for one user
10 | type Session struct {
11 | gorm.Model
12 | Token string `gorm:"size:255;unique_index"`
13 | UserID int
14 | User User
15 | }
16 |
17 | // CreateSession is a method which creates a session using gorm
18 | func CreateSession(db *gorm.DB, userID int) Session {
19 | token, err := utility.GenerateToken(255)
20 | utility.ErrorHandler(err, false)
21 |
22 | session := Session{
23 | Token: token,
24 | UserID: userID,
25 | }
26 |
27 | if err := db.Create(&session).Error; err != nil {
28 | fmt.Println("Could not create session")
29 | }
30 |
31 | return session
32 | }
33 |
34 | // FetchUserFromSession takes a provided token string and fetches the user for the session matching the provided token
35 | func FetchUserFromSession(db *gorm.DB, token string) User {
36 | user := User{}
37 | session := Session{}
38 | db.Where("token = ?", token).First(&session)
39 | if session.ID != 0 {
40 | user = FetchUser(db, session.UserID)
41 | }
42 | return user
43 | }
44 |
--------------------------------------------------------------------------------
/pkg/structs/setting.go:
--------------------------------------------------------------------------------
1 | package structs
2 |
3 | import (
4 | "fmt"
5 | "gorm.io/gorm"
6 | )
7 |
8 | // Setting represents a key value setting for Beubo usually used for global config values
9 | type Setting struct {
10 | gorm.Model
11 | Key string `gorm:"size:255"`
12 | Value string `gorm:"size:255"`
13 | }
14 |
15 | // CreateSetting is a method which creates a setting using gorm
16 | func CreateSetting(db *gorm.DB, key string, value string) bool {
17 | setting := Setting{
18 | Key: key,
19 | Value: value,
20 | }
21 |
22 | if err := db.Create(&setting).Error; err != nil {
23 | fmt.Println("Could not create setting")
24 | return false
25 | }
26 | return true
27 | }
28 |
29 | // FetchSetting gets a setting from the database via the provided id
30 | func FetchSetting(db *gorm.DB, id int) Setting {
31 | setting := Setting{}
32 | db.First(&setting, id)
33 | return setting
34 | }
35 |
36 | // FetchSettingByKey gets a setting from the database via the provided key
37 | func FetchSettingByKey(db *gorm.DB, key string) Setting {
38 | setting := Setting{}
39 | db.Where("key = ?", key).First(&setting)
40 | return setting
41 | }
42 |
43 | // FetchSettings gets all settings from the database
44 | func FetchSettings(db *gorm.DB) (settings []Setting) {
45 | db.Find(&settings)
46 | return settings
47 | }
48 |
49 | // UpdateSetting updates a setting key value pair using gorm
50 | func UpdateSetting(db *gorm.DB, id int, key string, value string) bool {
51 | setting := FetchSetting(db, id)
52 |
53 | setting.Key = key
54 | setting.Value = value
55 |
56 | if err := db.Save(&setting).Error; err != nil {
57 | fmt.Println("Could not create setting")
58 | return false
59 | }
60 | return true
61 | }
62 |
63 | // DeleteSetting removes a setting with the matching id from the database
64 | func DeleteSetting(db *gorm.DB, id int) Setting {
65 | setting := FetchSetting(db, id)
66 | db.Delete(&setting)
67 | return setting
68 | }
69 |
--------------------------------------------------------------------------------
/pkg/structs/site.go:
--------------------------------------------------------------------------------
1 | package structs
2 |
3 | import (
4 | "fmt"
5 | "gorm.io/gorm"
6 | )
7 |
8 | // Site represents one website, the idea is that Beubo handles many websites at the same time, you could then have 100s of sites all on the same platform
9 | type Site struct {
10 | gorm.Model
11 | Title string `gorm:"size:255"`
12 | Domain string `gorm:"size:255;unique_index"`
13 | DestinationDomain string
14 | Type int
15 | Theme Theme
16 | ThemeID int
17 | Users []*User `gorm:"many2many:user_sites;"`
18 | }
19 |
20 | // CreateSite is a method which creates a site using gorm
21 | func CreateSite(db *gorm.DB, title string, domain string, siteType int, themeID int, destinationDomain string) Site {
22 | site := Site{
23 | Title: title,
24 | Domain: domain,
25 | Type: siteType,
26 | ThemeID: themeID,
27 | DestinationDomain: destinationDomain,
28 | }
29 |
30 | if err := db.Create(&site).Error; err != nil {
31 | fmt.Println("Could not create site")
32 | return site
33 | }
34 | return site
35 | }
36 |
37 | // FetchSite gets a site from the database using the provided id
38 | func FetchSite(db *gorm.DB, id int) (site Site) {
39 | site = Site{}
40 | db.Preload("Theme").First(&site, id)
41 | return site
42 | }
43 |
44 | // FetchSites gets a site from the database using the provided id
45 | func FetchSites(db *gorm.DB) (sites []Site) {
46 | sites = []Site{}
47 | db.Find(&sites)
48 | return sites
49 | }
50 |
51 | // FetchSiteByHost retrieves a site from the database based on the provided host string
52 | // TODO what if one site can have many hosts? For now a redirect can be added for other hosts
53 | func FetchSiteByHost(db *gorm.DB, host string) Site {
54 | site := Site{}
55 | db.Preload("Theme").Where("domain = ?", host).First(&site)
56 | return site
57 | }
58 |
59 | // UpdateSite is a method which updates a site using gorm
60 | func UpdateSite(db *gorm.DB, id int, title string, domain string, siteType int, themeID int, destinationDomain string) bool {
61 | site := FetchSite(db, id)
62 |
63 | site.Title = title
64 | site.Domain = domain
65 | site.Type = siteType
66 | site.ThemeID = themeID
67 | site.DestinationDomain = destinationDomain
68 |
69 | if err := db.Save(&site).Error; err != nil {
70 | fmt.Println("Could not create site")
71 | return false
72 | }
73 | return true
74 | }
75 |
76 | // DeleteSite removes a site from the database based on the provided id
77 | func DeleteSite(db *gorm.DB, id int) Site {
78 | site := FetchSite(db, id)
79 | db.Delete(&site)
80 | return site
81 | }
82 |
--------------------------------------------------------------------------------
/pkg/structs/theme.go:
--------------------------------------------------------------------------------
1 | package structs
2 |
3 | import "gorm.io/gorm"
4 |
5 | // Theme is the template or html files that a site uses, theme files are found under the web directory
6 | type Theme struct {
7 | gorm.Model
8 | Title string `gorm:"size:255;unique_index"`
9 | Slug string `gorm:"size:255;unique_index"`
10 | }
11 |
12 | // FetchTheme gets a theme from the database using the provided id
13 | func FetchTheme(db *gorm.DB, id int) Theme {
14 | theme := Theme{}
15 |
16 | db.First(&theme, id)
17 |
18 | return theme
19 | }
20 |
21 | // FetchThemeBySlug gets a theme from the database by the slug string
22 | func FetchThemeBySlug(db *gorm.DB, slug string) Theme {
23 | theme := Theme{}
24 |
25 | db.Where("slug = ?", slug).First(&theme)
26 |
27 | return theme
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/structs/user.go:
--------------------------------------------------------------------------------
1 | package structs
2 |
3 | import (
4 | "fmt"
5 | "github.com/uberswe/beubo/pkg/utility"
6 | "golang.org/x/crypto/bcrypt"
7 | "gorm.io/gorm"
8 | "log"
9 | )
10 |
11 | // User is a user who can authenticate with Beubo
12 | type User struct {
13 | gorm.Model
14 | Email string `gorm:"size:255"`
15 | Password string `gorm:"size:255"`
16 | Activations []UserActivation
17 | Roles []*Role `gorm:"many2many:user_roles;"`
18 | Sites []*Site `gorm:"many2many:user_sites;"`
19 | }
20 |
21 | // UserActivation is used to verify a user when signing up
22 | type UserActivation struct {
23 | gorm.Model
24 | UserID uint
25 | Type string // Email, SMS, PushNotification
26 | Active bool
27 | Code string
28 | }
29 |
30 | // CanAccess checks if a user has access to the specified feature
31 | func (u User) CanAccess(db *gorm.DB, featureKey string) bool {
32 | var count int64
33 | db.Model(&Feature{}).
34 | Select("features.id").
35 | Joins("left join role_features on role_features.feature_id = features.id").
36 | Joins("left join user_roles on user_roles.role_id = role_features.role_id").
37 | Where("features.key = ?", featureKey).
38 | Where("user_roles.user_id = ?", u.ID).
39 | Count(&count)
40 | return count > 0
41 | }
42 |
43 | // CanAccessSite checks if a user is allowed to access the specified site
44 | func (u User) CanAccessSite(db *gorm.DB, site Site) bool {
45 | var count int64
46 | db.Model(&Site{}).
47 | Select("sites.id").
48 | Joins("left join user_sites on user_sites.site_id = sites.id").
49 | Where("user_sites.user_id = ?", u.ID).
50 | Where("sites.id = ?", site.ID).
51 | Count(&count)
52 | return count > 0
53 | }
54 |
55 | // HasRole checks if a user has the specified role
56 | func (u User) HasRole(db *gorm.DB, r Role) bool {
57 | roles := []Role{}
58 | _ = db.Model(&u).Where("roles.name = ?", r.Name).Association("Roles").Find(&roles)
59 | if len(roles) >= 1 {
60 | return true
61 | }
62 | return false
63 | }
64 |
65 | // CreateUser is a method which creates a user using gorm
66 | func CreateUser(db *gorm.DB, email string, password string, roles []*Role, sites []*Site) bool {
67 | checkUser := FetchUserByEmail(db, email)
68 | if checkUser.ID != 0 {
69 | return false
70 | }
71 | user := User{
72 | Email: email,
73 | Roles: roles,
74 | Sites: sites,
75 | }
76 | if err := db.Create(&user).Error; err != nil {
77 | fmt.Println("Could not create user")
78 | return false
79 | }
80 | // User password is hashed after the response is returned to improve performance
81 | go hashUserPassword(db, user, password)
82 | return true
83 | }
84 |
85 | // FetchUser retrieves a user from the database using the provided id
86 | func FetchUser(db *gorm.DB, id int) User {
87 | user := User{}
88 | db.First(&user, id)
89 | return user
90 | }
91 |
92 | // FetchUserByEmail retrieves a user from the database using the provided email
93 | func FetchUserByEmail(db *gorm.DB, email string) User {
94 | user := User{}
95 | db.Where("email = ?", email).First(&user)
96 | return user
97 | }
98 |
99 | // hashUserPassword hashes the user password using bcrypt
100 | func hashUserPassword(db *gorm.DB, user User, password string) {
101 |
102 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 14)
103 |
104 | if err != nil {
105 | fmt.Println("Password hashing failed for user")
106 | return
107 | }
108 |
109 | user.Password = string(hashedPassword)
110 |
111 | if err := db.Save(&user).Error; err != nil {
112 | fmt.Println("Could not update hashed password for user")
113 | }
114 | }
115 |
116 | // AuthUser authenticates the user by verifying a username and password
117 | func AuthUser(db *gorm.DB, email string, password string) *User {
118 | user := User{}
119 |
120 | if err := db.Where("email = ?", email).First(&user).Error; err != nil {
121 | log.Println(err)
122 | return nil
123 | }
124 |
125 | err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
126 | if err == bcrypt.ErrMismatchedHashAndPassword {
127 | return nil
128 | } else if err != nil {
129 | return nil
130 | }
131 |
132 | return &user
133 | }
134 |
135 | // UpdateUser updates the user struct with the provided details
136 | func UpdateUser(db *gorm.DB, id int, email string, password string, roles []*Role, sites []*Site) bool {
137 | user := FetchUser(db, id)
138 | checkUser := FetchUserByEmail(db, email)
139 | if checkUser.ID != 0 && checkUser.ID != user.ID {
140 | return false
141 | }
142 | err := db.Model(&user).Association("Roles").Clear()
143 | utility.ErrorHandler(err, false)
144 | err = db.Model(&user).Association("Sites").Clear()
145 | utility.ErrorHandler(err, false)
146 | user.Email = email
147 | user.Roles = roles
148 | user.Sites = sites
149 | if err := db.Save(&user).Error; err != nil {
150 | fmt.Println("Could not create user")
151 | return false
152 | }
153 | // User password is hashed after the response is returned to improve performance
154 | // An empty password updates the user without changing the password
155 | if len(password) > 8 {
156 | go hashUserPassword(db, user, password)
157 | }
158 | return true
159 | }
160 |
161 | // DeleteUser deletes a user by id
162 | func DeleteUser(db *gorm.DB, id int) (user User) {
163 | db.First(&user, id)
164 | db.Delete(&user)
165 | return user
166 | }
167 |
--------------------------------------------------------------------------------
/pkg/template/page.go:
--------------------------------------------------------------------------------
1 | package template
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | // GetPageTemplates recursively parses the themes directory to find all theme files
8 | func (btr *BeuboTemplateRenderer) GetPageTemplates() map[string]string {
9 | pageTemplates := map[string]string{
10 | "page": "Default",
11 | }
12 | for _, t := range btr.T.Templates() {
13 | // Ignore paths and only look at names containing .page. and does not contain .admin.
14 | if !strings.Contains(t.Name(), "themes/") && !strings.Contains(t.Name(), ".admin.") && strings.Contains(t.Name(), ".page.") {
15 | s := strings.Split(t.Name(), ".")
16 | pageName := ""
17 | pageTemplate := "page"
18 | pageFound := false
19 | for _, part := range s {
20 | if pageFound {
21 | if len(pageName) > 0 {
22 | pageName += " "
23 | }
24 | pageTemplate += "." + part
25 | pageName += strings.Title(part)
26 | }
27 | // Anything after page is our page template name
28 | if part == "page" {
29 | pageFound = true
30 | }
31 | }
32 | if len(pageName) > 0 {
33 | pageTemplates[pageTemplate] = pageName
34 | }
35 | }
36 | }
37 | return pageTemplates
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/template/render.go:
--------------------------------------------------------------------------------
1 | package template
2 |
3 | import (
4 | "fmt"
5 | "github.com/uberswe/beubo/pkg/middleware"
6 | "github.com/uberswe/beubo/pkg/plugin"
7 | "github.com/uberswe/beubo/pkg/structs"
8 | "github.com/uberswe/beubo/pkg/structs/page"
9 | "github.com/uberswe/beubo/pkg/structs/page/menu"
10 | "github.com/uberswe/beubo/pkg/utility"
11 | "gorm.io/gorm"
12 | "html/template"
13 | "io/ioutil"
14 | "log"
15 | "net/http"
16 | "os"
17 | "path/filepath"
18 | "strconv"
19 | "strings"
20 | "time"
21 | )
22 |
23 | var (
24 | currentTheme = "default"
25 | rootDir = "./"
26 | )
27 |
28 | // BeuboTemplateRenderer holds all the configuration variables for rendering templates in Beubo
29 | type BeuboTemplateRenderer struct {
30 | T *template.Template
31 | PluginHandler *plugin.Handler
32 | ReloadTemplates bool
33 | CurrentTheme string
34 | ThemeDir string
35 | DB *gorm.DB
36 | }
37 |
38 | // Init prepares the BeuboTemplateRenderer to render pages with html templates
39 | func (btr *BeuboTemplateRenderer) Init() {
40 | log.Println("Parsing and loading templates...")
41 | var err error
42 | btr.T, err = findAndParseTemplates(rootDir)
43 | utility.ErrorHandler(err, false)
44 | }
45 |
46 | // RenderHTMLPage handles rendering of the html template and should be the last function called before returning the response
47 | func (btr *BeuboTemplateRenderer) RenderHTMLPage(w http.ResponseWriter, r *http.Request, pageData structs.PageData) {
48 |
49 | if os.Getenv("ASSETS_DIR") != "" {
50 | rootDir = os.Getenv("ASSETS_DIR")
51 | }
52 |
53 | // Session flash messages to prompt failed logins etc..
54 | errorMessage, err := utility.GetFlash(w, r, "error")
55 | utility.ErrorHandler(err, false)
56 | warningMessage, err := utility.GetFlash(w, r, "warning")
57 | utility.ErrorHandler(err, false)
58 | stringMessage, err := utility.GetFlash(w, r, "message")
59 | utility.ErrorHandler(err, false)
60 |
61 | siteName := "Beubo"
62 |
63 | // Get the site from context
64 | site := r.Context().Value(middleware.SiteContextKey)
65 | user := r.Context().Value(middleware.UserContextKey)
66 |
67 | if site != nil {
68 | btr.CurrentTheme = site.(structs.Site).Theme.Slug
69 | siteName = site.(structs.Site).Title
70 | } else if os.Getenv("THEME") != "" {
71 | btr.CurrentTheme = os.Getenv("THEME")
72 | } else {
73 | // Default theme
74 | if btr.CurrentTheme == "" {
75 | btr.CurrentTheme = "default"
76 | }
77 | }
78 |
79 | // TODO in the future we should make some way for the theme to define the stylesheets
80 | scripts := []string{
81 | "/default/js/main.js",
82 | }
83 |
84 | stylesheets := []string{
85 | "/default/css/main.css",
86 | }
87 |
88 | if strings.HasPrefix(r.URL.Path, "/admin") {
89 | scripts = append(scripts, "/default/js/admin.js")
90 | stylesheets = append(stylesheets, "/default/css/admin.css")
91 | }
92 | var userFromContext structs.User
93 | if user != nil && user.(structs.User).ID > 0 {
94 | userFromContext = user.(structs.User)
95 | }
96 |
97 | data := structs.PageData{
98 | Stylesheets: stylesheets,
99 | // TODO make the favicon dynamic
100 | Favicon: "/default/images/favicon.ico",
101 | Scripts: scripts,
102 | WebsiteName: siteName,
103 | URL: "http://localhost:3000",
104 | Error: string(errorMessage),
105 | Warning: string(warningMessage),
106 | Message: string(stringMessage),
107 | Year: strconv.Itoa(time.Now().Year()),
108 | Menus: btr.buildMenus(userFromContext),
109 | }
110 |
111 | data = mergePageData(data, pageData)
112 | // PluginHandler is not available during installation
113 | if btr.PluginHandler != nil {
114 | data = btr.PluginHandler.PageData(r, data)
115 | }
116 |
117 | if btr.ReloadTemplates {
118 | log.Println("Parsing and loading templates...")
119 | var err error
120 | btr.T, err = findAndParseTemplates(rootDir)
121 | utility.ErrorHandler(err, false)
122 | }
123 |
124 | var foundTemplate *template.Template
125 |
126 | path := fmt.Sprintf("%s.%s", btr.CurrentTheme, pageData.Template)
127 | if foundTemplate = btr.T.Lookup(path); foundTemplate == nil {
128 | // Fallback to default
129 | path := fmt.Sprintf("%s.%s", "default", pageData.Template)
130 | if foundTemplate = btr.T.Lookup(path); foundTemplate == nil {
131 | log.Printf("Theme file not found %s\n", path)
132 | return
133 | }
134 | }
135 |
136 | err = foundTemplate.Execute(w, data)
137 | utility.ErrorHandler(err, false)
138 | }
139 |
140 | func (btr *BeuboTemplateRenderer) buildMenus(user structs.User) []page.Menu {
141 | menuItems := []page.MenuItem{
142 | {Text: "Home", URI: "/"},
143 | {Text: "Login", URI: "/login"},
144 | }
145 |
146 | // DB is not available during installation
147 | if btr.DB != nil {
148 | setting := structs.FetchSettingByKey(btr.DB, "enable_user_registration")
149 | if !(setting.ID == 0 || setting.Value == "false") {
150 | menuItems = append(menuItems, page.MenuItem{Text: "Register", URI: "/register"})
151 | }
152 | }
153 |
154 | menus := []page.Menu{menu.DefaultMenu{
155 | Items: menuItems,
156 | Identifier: "header",
157 | T: btr.T,
158 | }}
159 |
160 | if user.ID > 0 {
161 | adminHeaderMenu := []page.MenuItem{
162 | {Text: "Home", URI: "/"},
163 | {Text: "Logout", URI: "/logout"},
164 | }
165 |
166 | if user.CanAccess(btr.DB, "manage_sites") ||
167 | user.CanAccess(btr.DB, "manage_pages") ||
168 | user.CanAccess(btr.DB, "manage_users") ||
169 | user.CanAccess(btr.DB, "manage_user_roles") ||
170 | user.CanAccess(btr.DB, "manage_plugins") ||
171 | user.CanAccess(btr.DB, "manage_settings") {
172 | adminHeaderMenu = []page.MenuItem{
173 | {Text: "Home", URI: "/"},
174 | {Text: "Admin", URI: "/admin"},
175 | {Text: "Logout", URI: "/logout"},
176 | }
177 | }
178 |
179 | adminSidebarMenu := []page.MenuItem{}
180 |
181 | if user.CanAccess(btr.DB, "manage_sites") {
182 | adminSidebarMenu = append(adminSidebarMenu, page.MenuItem{Text: "Sites", URI: "/admin/"})
183 | }
184 |
185 | if user.CanAccess(btr.DB, "manage_settings") {
186 | adminSidebarMenu = append(adminSidebarMenu, page.MenuItem{Text: "Settings", URI: "/admin/settings"})
187 | }
188 |
189 | if user.CanAccess(btr.DB, "manage_users") {
190 | adminSidebarMenu = append(adminSidebarMenu, page.MenuItem{
191 | Text: "Users",
192 | URI: "/admin/users",
193 | Items: []page.MenuItem{
194 | {
195 | Text: "Roles",
196 | URI: "/admin/users/roles",
197 | },
198 | },
199 | // Submenus need template to be defined
200 | T: btr.T,
201 | })
202 | }
203 |
204 | if user.CanAccess(btr.DB, "manage_plugins") {
205 | adminSidebarMenu = append(adminSidebarMenu, page.MenuItem{Text: "Plugins", URI: "/admin/plugins"})
206 | }
207 |
208 | menus = []page.Menu{menu.DefaultMenu{
209 | Items: adminHeaderMenu,
210 | Identifier: "header",
211 | T: btr.T,
212 | }, menu.DefaultMenu{
213 | Items: adminSidebarMenu,
214 | Identifier: "sidebar",
215 | Template: "menu.sidebar",
216 | T: btr.T,
217 | }}
218 | }
219 | return menus
220 | }
221 |
222 | // findAndParseTemplates finds all the templates in the rootDir and makes a template map
223 | // This method was found here https://stackoverflow.com/a/50581032/1260548
224 | func findAndParseTemplates(rootDir string) (*template.Template, error) {
225 | cleanRoot, err := filepath.Abs(rootDir)
226 | if err != nil {
227 | return nil, err
228 | }
229 | pfx := len(cleanRoot) + 1
230 | root := template.New("")
231 |
232 | err = filepath.Walk(cleanRoot, func(path string, info os.FileInfo, e1 error) error {
233 | if !info.IsDir() && strings.HasSuffix(path, ".html") {
234 | if e1 != nil {
235 | return e1
236 | }
237 |
238 | b, e2 := ioutil.ReadFile(path)
239 | if e2 != nil {
240 | return e2
241 | }
242 |
243 | name := path[pfx:]
244 | t := root.New(name)
245 | t, e2 = t.Parse(string(b))
246 | if e2 != nil {
247 | return e2
248 | }
249 | }
250 |
251 | return nil
252 | })
253 |
254 | return root, err
255 | }
256 |
257 | func mergePageData(a structs.PageData, b structs.PageData) structs.PageData {
258 | // TODO this could be simplified by making a function that compares an interface and picks a value but I decided that this is more readable for now
259 | if b.Template != "" {
260 | a.Template = b.Template
261 | }
262 |
263 | if b.Title != "" {
264 | a.Title = b.Title
265 | }
266 |
267 | if b.WebsiteName != "" {
268 | a.WebsiteName = b.WebsiteName
269 | }
270 |
271 | if b.Error != "" {
272 | a.Error = b.Error
273 | }
274 |
275 | if b.Warning != "" {
276 | a.Warning = b.Warning
277 | }
278 |
279 | if b.Message != "" {
280 | a.Message = b.Message
281 | }
282 |
283 | if len(b.Scripts) > 0 {
284 | a.Scripts = b.Scripts
285 | }
286 |
287 | if len(b.Stylesheets) > 0 {
288 | a.Stylesheets = b.Stylesheets
289 | }
290 |
291 | if b.Favicon != "" {
292 | a.Favicon = b.Favicon
293 | }
294 |
295 | if len(b.Menus) > 0 {
296 | a.Menus = b.Menus
297 | }
298 |
299 | a.Components = b.Components
300 | a.Themes = b.Themes
301 | a.Templates = b.Templates
302 | a.Extra = b.Extra
303 |
304 | return a
305 | }
306 |
--------------------------------------------------------------------------------
/pkg/template/site.go:
--------------------------------------------------------------------------------
1 | package template
2 |
3 | import (
4 | "io/ioutil"
5 | "log"
6 | )
7 |
8 | // GetThemes fetches all the themes in the theme directory as strings
9 | func (btr *BeuboTemplateRenderer) GetThemes() map[string]string {
10 | pageThemes := map[string]string{}
11 |
12 | files, err := ioutil.ReadDir(btr.ThemeDir)
13 |
14 | if err != nil {
15 | log.Println(err)
16 | return pageThemes
17 | }
18 |
19 | for _, file := range files {
20 | // TODO in the future we could define a way to define names for themes
21 | // Ignore the install directory, only used for installation
22 | if file.IsDir() && file.Name() != "install" {
23 | pageThemes[file.Name()] = file.Name()
24 | }
25 | }
26 |
27 | return pageThemes
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/utility/email.go:
--------------------------------------------------------------------------------
1 | package utility
2 |
3 | import "regexp"
4 |
5 | // from https://golangcode.com/validate-an-email-address/
6 |
7 | var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
8 |
9 | // IsEmailValid checks if the email provided passes the required structure and length.
10 | func IsEmailValid(e string) bool {
11 | if len(e) < 3 && len(e) > 254 {
12 | return false
13 | }
14 | return emailRegex.MatchString(e)
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/utility/errors.go:
--------------------------------------------------------------------------------
1 | package utility
2 |
3 | import "log"
4 |
5 | // ErrorHandler is a utility function to handle errors so we don't have to write
6 | // if err != nil {
7 | // // Do something here
8 | // }
9 | func ErrorHandler(e error, fatal bool) bool {
10 | if e != nil {
11 | if fatal {
12 | log.Fatal(e)
13 | } else {
14 | log.Print(e)
15 | }
16 | return true
17 | }
18 | return false
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/utility/requests.go:
--------------------------------------------------------------------------------
1 | package utility
2 |
3 | import (
4 | "encoding/base64"
5 | "net/http"
6 | "time"
7 | )
8 |
9 | // GetFlash gets the cookie value set by SetFlash and removes the cookie
10 | func GetFlash(w http.ResponseWriter, r *http.Request, name string) ([]byte, error) {
11 | c, err := r.Cookie(name)
12 | if err != nil {
13 | switch err {
14 | case http.ErrNoCookie:
15 | return nil, nil
16 | default:
17 | return nil, err
18 | }
19 | }
20 | value, err := Decode(c.Value)
21 | if err != nil {
22 | return nil, err
23 | }
24 | // Deletes the cookie
25 | dc := &http.Cookie{Path: "/", Name: name, MaxAge: -1, Expires: time.Now().Add(-100 * time.Hour)}
26 | http.SetCookie(w, dc)
27 | return value, nil
28 | }
29 |
30 | // SetFlash sets a cookie which expires after the next page load
31 | func SetFlash(w http.ResponseWriter, name string, value []byte) {
32 | c := &http.Cookie{Path: "/", Name: name, Value: Encode(value)}
33 | http.SetCookie(w, c)
34 | }
35 |
36 | // Encode encodes a byte array into an urlencoded string
37 | func Encode(src []byte) string {
38 | return base64.URLEncoding.EncodeToString(src)
39 | }
40 |
41 | // Decode decodes an url encoded string into a byte array
42 | func Decode(src string) ([]byte, error) {
43 | return base64.URLEncoding.DecodeString(src)
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/utility/strings.go:
--------------------------------------------------------------------------------
1 | package utility
2 |
3 | import (
4 | "crypto/rand"
5 | "fmt"
6 | "strings"
7 | "unicode"
8 | )
9 |
10 | // GenerateToken generates a random string of len length
11 | func GenerateToken(len int) (string, error) {
12 | b := make([]byte, len)
13 | _, err := rand.Read(b)
14 | if err != nil {
15 | return "", err
16 | }
17 | return fmt.Sprintf("%x", b), nil
18 | }
19 |
20 | // TrimWhitespace takes a string and removes any spaces
21 | func TrimWhitespace(str string) string {
22 | var b strings.Builder
23 | b.Grow(len(str))
24 | for _, ch := range str {
25 | if !unicode.IsSpace(ch) {
26 | b.WriteRune(ch)
27 | }
28 | }
29 | return b.String()
30 | }
31 |
--------------------------------------------------------------------------------
/plugin.go:
--------------------------------------------------------------------------------
1 | package beubo
2 |
3 | import (
4 | beuboplugin "github.com/uberswe/beubo/pkg/plugin"
5 | )
6 |
7 | // This is the plugin handler for Beubo
8 | // The initial function will always be Register
9 | // We need a fairly dynamic way to hook any Beubo function into the plugins
10 | // Most functions can be async but some will cause Beubo to wait for them to finish.
11 | // For example, an analytics plugin does not need Beubo to wait as it doesn't affect the flow
12 | // But an ecommerce plugin would need Beubo to wait since it affects what is displayed on the page
13 |
14 | var pluginHandler beuboplugin.Handler
15 |
16 | func loadPlugins() {
17 | pluginHandler = beuboplugin.Handler{
18 | DB: DB,
19 | }
20 | pluginHandler = beuboplugin.Load(pluginHandler)
21 | }
22 |
--------------------------------------------------------------------------------
/plugins/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !README.md
--------------------------------------------------------------------------------
/plugins/README.md:
--------------------------------------------------------------------------------
1 | ## Plugins
2 |
3 | Beubo supports plugins and it should use the Go plugins package https://golang.org/pkg/plugin/
4 |
5 | More documentation, sample code and a proper interface will be added.
6 |
7 | ## Build a plugin
8 |
9 | To run a plugin it needs to be compiled, to build the plugin run:
10 |
11 | ```
12 | go build -buildmode=plugin
13 | ```
14 |
15 | ## Available functions
16 |
17 | Currently the only function available is the `Register` function which is called when Beubo starts.
18 | This can be used to tell Beubo about the plugin.
19 |
20 | ## Planned functions
21 |
22 | - Page request
23 | - Template
24 | - Save page
25 | - New page
26 | - New site
27 | - Save site
28 | - Login
29 | - Logout
30 | - Install started
31 | - Install finished
32 | - Beubo is running, a run function that starts running if Beubo is active
33 | - Shutdown
--------------------------------------------------------------------------------
/routes.go:
--------------------------------------------------------------------------------
1 | package beubo
2 |
3 | import (
4 | "fmt"
5 | "github.com/goincremental/negroni-sessions"
6 | "github.com/goincremental/negroni-sessions/cookiestore"
7 | "github.com/gorilla/mux"
8 | "github.com/uberswe/beubo/pkg/middleware"
9 | "github.com/uberswe/beubo/pkg/routes"
10 | "github.com/uberswe/beubo/pkg/template"
11 | "github.com/uberswe/beubo/pkg/utility"
12 | "github.com/urfave/negroni"
13 | "golang.org/x/time/rate"
14 | "io/ioutil"
15 | "log"
16 | "net/http"
17 | "sync"
18 | "time"
19 | )
20 |
21 | var themes []string
22 | var fileServers = map[string]http.Handler{}
23 |
24 | // routesInit initializes the routes and starts a web server that listens on the specified port
25 | func routesInit() {
26 | var err error
27 |
28 | beuboTemplateRenderer := template.BeuboTemplateRenderer{
29 | ReloadTemplates: reloadTemplates,
30 | CurrentTheme: currentTheme,
31 | ThemeDir: rootDir,
32 | PluginHandler: &pluginHandler,
33 | DB: DB,
34 | }
35 | beuboTemplateRenderer.Init()
36 |
37 | beuboRouter := &routes.BeuboRouter{
38 | DB: DB,
39 | Renderer: &beuboTemplateRenderer,
40 | PluginHandler: &pluginHandler,
41 | }
42 |
43 | beuboMiddleware := &middleware.BeuboMiddleware{
44 | DB: DB,
45 | PluginHandler: &pluginHandler,
46 | }
47 |
48 | // TODO make the burst and time configurable
49 | throttleMiddleware := middleware.Throttle{
50 | IPs: make(map[string]middleware.ThrottleClient),
51 | Rate: rate.Every(time.Minute),
52 | Burst: 300,
53 | Mu: &sync.RWMutex{},
54 | Cleanup: time.Duration(24) * time.Hour,
55 | }
56 |
57 | utility.ErrorHandler(err, true)
58 |
59 | r := mux.NewRouter()
60 | r.StrictSlash(true)
61 | n := negroni.Classic()
62 |
63 | store := cookiestore.New(sessionKey)
64 | n.Use(sessions.Sessions("beubo", store))
65 |
66 | r.NotFoundHandler = http.HandlerFunc(beuboRouter.PageHandler)
67 |
68 | log.Println("Registering themes...")
69 |
70 | r = registerStaticFiles(r)
71 |
72 | log.Println("Registering routes...")
73 |
74 | r.HandleFunc("/login", beuboRouter.Login).Methods("GET")
75 | r.HandleFunc("/login", beuboRouter.LoginPost).Methods("POST")
76 |
77 | r.HandleFunc("/register", beuboRouter.Register).Methods("GET")
78 | r.HandleFunc("/register", beuboRouter.RegisterPost).Methods("POST")
79 |
80 | admin := r.PathPrefix("/admin").Subrouter()
81 | admin.HandleFunc("/", beuboRouter.Admin)
82 |
83 | admin.HandleFunc("/settings", beuboRouter.Settings)
84 | admin.HandleFunc("/users", beuboRouter.Users)
85 | admin.HandleFunc("/users/roles", beuboRouter.AdminUserRoles)
86 | admin.HandleFunc("/plugins", beuboRouter.Plugins)
87 |
88 | admin.HandleFunc("/sites/add", beuboRouter.AdminSiteAdd).Methods("GET")
89 | admin.HandleFunc("/sites/add", beuboRouter.AdminSiteAddPost).Methods("POST")
90 |
91 | admin.HandleFunc("/settings/add", beuboRouter.AdminSettingAdd).Methods("GET")
92 | admin.HandleFunc("/settings/add", beuboRouter.AdminSettingAddPost).Methods("POST")
93 |
94 | admin.HandleFunc("/users/add", beuboRouter.AdminUserAdd).Methods("GET")
95 | admin.HandleFunc("/users/add", beuboRouter.AdminUserAddPost).Methods("POST")
96 |
97 | admin.HandleFunc("/users/roles/add", beuboRouter.AdminUserRoleAdd).Methods("GET")
98 | admin.HandleFunc("/users/roles/add", beuboRouter.AdminUserRoleAddPost).Methods("POST")
99 |
100 | admin.HandleFunc("/sites/delete/{id:[0-9]+}", beuboRouter.AdminSiteDelete)
101 | admin.HandleFunc("/settings/delete/{id:[0-9]+}", beuboRouter.AdminSettingDelete)
102 | admin.HandleFunc("/users/delete/{id:[0-9]+}", beuboRouter.AdminUserDelete)
103 | admin.HandleFunc("/users/roles/delete/{id:[0-9]+}", beuboRouter.AdminUserRoleDelete)
104 |
105 | admin.HandleFunc("/sites/edit/{id:[0-9]+}", beuboRouter.AdminSiteEdit).Methods("GET")
106 | admin.HandleFunc("/sites/edit/{id:[0-9]+}", beuboRouter.AdminSiteEditPost).Methods("POST")
107 |
108 | admin.HandleFunc("/settings/edit/{id:[0-9]+}", beuboRouter.AdminSettingEdit).Methods("GET")
109 | admin.HandleFunc("/settings/edit/{id:[0-9]+}", beuboRouter.AdminSettingEditPost).Methods("POST")
110 |
111 | admin.HandleFunc("/users/edit/{id:[0-9]+}", beuboRouter.AdminUserEdit).Methods("GET")
112 | admin.HandleFunc("/users/edit/{id:[0-9]+}", beuboRouter.AdminUserEditPost).Methods("POST")
113 |
114 | admin.HandleFunc("/users/roles/edit/{id:[0-9]+}", beuboRouter.AdminUserRoleEdit).Methods("GET")
115 | admin.HandleFunc("/users/roles/edit/{id:[0-9]+}", beuboRouter.AdminUserRoleEditPost).Methods("POST")
116 |
117 | admin.HandleFunc("/plugins/edit/{id:[a-zA-Z_]+}", beuboRouter.AdminPluginEdit).Methods("GET")
118 | admin.HandleFunc("/plugins/edit/{id:[a-zA-Z_]+}", beuboRouter.AdminPluginEditPost).Methods("POST")
119 |
120 | // TODO I don't like this /sites/a/ structure of the routes, consider changing it
121 | siteAdmin := admin.PathPrefix("/sites/a/{id:[0-9]+}").Subrouter()
122 |
123 | siteAdmin.HandleFunc("/", beuboRouter.SiteAdmin)
124 |
125 | siteAdmin.HandleFunc("/page/new", beuboRouter.SiteAdminPageNew).Methods("GET")
126 | siteAdmin.HandleFunc("/page/new", beuboRouter.SiteAdminPageNewPost).Methods("POST")
127 |
128 | siteAdmin.HandleFunc("/page/edit/{pageId:[0-9]+}", beuboRouter.AdminSitePageEdit).Methods("GET")
129 | siteAdmin.HandleFunc("/page/edit/{pageId:[0-9]+}", beuboRouter.AdminSitePageEditPost).Methods("POST")
130 |
131 | siteAdmin.HandleFunc("/page/delete/{pageId:[0-9]+}", beuboRouter.AdminSitePageDelete)
132 |
133 | r.HandleFunc("/logout", beuboRouter.Logout)
134 |
135 | muxer := http.NewServeMux()
136 | muxer.Handle("/", negroni.New(
137 | negroni.HandlerFunc(beuboMiddleware.Site),
138 | negroni.HandlerFunc(beuboMiddleware.Whitelist),
139 | negroni.HandlerFunc(beuboMiddleware.Auth),
140 | negroni.HandlerFunc(throttleMiddleware.Throttle),
141 | negroni.HandlerFunc(beuboMiddleware.Plugin),
142 | negroni.Wrap(r),
143 | ))
144 |
145 | n.UseHandler(muxer)
146 |
147 | log.Println("HTTP Server listening on:", port)
148 | err = http.ListenAndServe(fmt.Sprintf(":%d", port), n)
149 | if err != nil {
150 | log.Println(err)
151 | }
152 | }
153 |
154 | // registerStaticFiles handles the loading of all static files for all templates
155 | func registerStaticFiles(r *mux.Router) *mux.Router {
156 | var err error
157 |
158 | themedir := "themes/"
159 | files, err := ioutil.ReadDir(themedir)
160 | utility.ErrorHandler(err, false)
161 | for _, f := range files {
162 | if !f.IsDir() {
163 | continue
164 | }
165 | themes = append(themes, f.Name())
166 | // Register file paths for themes
167 | fileServers[f.Name()+"_css"] = http.FileServer(http.Dir(themedir + f.Name() + "/css/"))
168 | fileServers[f.Name()+"_js"] = http.FileServer(http.Dir(themedir + f.Name() + "/js/"))
169 | fileServers[f.Name()+"_images"] = http.FileServer(http.Dir(themedir + f.Name() + "/images/"))
170 | fileServers[f.Name()+"_fonts"] = http.FileServer(http.Dir(themedir + f.Name() + "/fonts/"))
171 |
172 | r.PathPrefix("/" + f.Name() + "/css/").Handler(http.StripPrefix("/"+f.Name()+"/css/", fileServers[f.Name()+"_css"]))
173 | r.PathPrefix("/" + f.Name() + "/js/").Handler(http.StripPrefix("/"+f.Name()+"/js/", fileServers[f.Name()+"_js"]))
174 | r.PathPrefix("/" + f.Name() + "/images/").Handler(http.StripPrefix("/"+f.Name()+"/images/", fileServers[f.Name()+"_images"]))
175 | r.PathPrefix("/" + f.Name() + "/favicon.ico").Handler(fileServers["/"+f.Name()+"_images"])
176 | r.PathPrefix("/" + f.Name() + "/fonts/").Handler(http.StripPrefix("/"+f.Name()+"/fonts/", fileServers[f.Name()+"_fonts"]))
177 | }
178 | return r
179 | }
180 |
--------------------------------------------------------------------------------
/settings.go:
--------------------------------------------------------------------------------
1 | package beubo
2 |
3 | import (
4 | "context"
5 | "encoding/hex"
6 | "errors"
7 | "fmt"
8 | "github.com/gorilla/mux"
9 | "github.com/gorilla/securecookie"
10 | "github.com/joho/godotenv"
11 | "github.com/uberswe/beubo/pkg/routes"
12 | "github.com/uberswe/beubo/pkg/structs"
13 | "github.com/uberswe/beubo/pkg/structs/page"
14 | "github.com/uberswe/beubo/pkg/structs/page/menu"
15 | "github.com/uberswe/beubo/pkg/template"
16 | "github.com/uberswe/beubo/pkg/utility"
17 | "github.com/urfave/negroni"
18 | "gorm.io/gorm"
19 | "gorm.io/gorm/logger"
20 | "io/ioutil"
21 | "log"
22 | "net/http"
23 | "os"
24 | "time"
25 | )
26 |
27 | var (
28 | databaseHost = "localhost"
29 | databaseName = ""
30 | databaseUser = ""
31 | databasePassword = ""
32 | databasePort = "3306"
33 | databaseDriver = "mysql"
34 | environment = "production"
35 | testuser = ""
36 | testpass = ""
37 |
38 | rootDir = "./themes/"
39 | currentTheme = "default"
40 | installed = false // TODO handle this in a middleware or something
41 | reloadTemplates = false
42 |
43 | sessionKey = securecookie.GenerateRandomKey(64)
44 |
45 | failures map[string]map[string]string
46 | )
47 |
48 | type logWriter struct {
49 | }
50 |
51 | func (writer logWriter) Write(bytes []byte) (int, error) {
52 | return fmt.Printf("[beubo] %s | %s", time.Now().Format("2006-01-02T15:04:05-07:00"), string(bytes))
53 | } //2006-01-02T15:04:05.999Z
54 |
55 | // Initializes the settings and checks if Beubo is installed by checking the env file
56 | // If no env file is present then this function will start it's own http listener to go through the installation process
57 | func settingsInit() {
58 |
59 | log.SetFlags(0)
60 | log.SetOutput(new(logWriter))
61 |
62 | err := godotenv.Load()
63 |
64 | if err != nil {
65 | // No .env file
66 | utility.ErrorHandler(err, false)
67 | log.Println("Attempting to create .env file")
68 | writeEnv("", "", "", "", "", "", "")
69 | }
70 |
71 | rootDir = setSetting(os.Getenv("ASSETS_DIR"), rootDir)
72 | currentTheme = setSetting(os.Getenv("THEME"), currentTheme)
73 |
74 | databaseHost = setSetting(os.Getenv("DB_HOST"), databaseHost)
75 | databaseName = setSetting(os.Getenv("DB_NAME"), databaseName)
76 | databaseUser = setSetting(os.Getenv("DB_USER"), databaseUser)
77 | databaseDriver = setSetting(os.Getenv("DB_DRIVER"), databaseDriver)
78 | databasePassword = setSetting(os.Getenv("DB_PASSWORD"), databasePassword)
79 | shouldRefreshDatabase = setBoolSetting(os.Getenv("REFRESH_DATABASE"), shouldRefreshDatabase)
80 | shouldSeed = setBoolSetting(os.Getenv("SEED_DATABASE"), shouldSeed)
81 |
82 | environment = setSetting(os.Getenv("ENVIRONMENT"), environment)
83 |
84 | testpass = setSetting(os.Getenv("TEST_PASS"), testpass)
85 | testuser = setSetting(os.Getenv("TEST_USER"), testuser)
86 |
87 | sessionKeyBytes, err := hex.DecodeString(os.Getenv("SESSION_KEY"))
88 | utility.ErrorHandler(err, false)
89 | if len(sessionKeyBytes) > 1 {
90 | sessionKey = sessionKeyBytes
91 | }
92 |
93 | if databaseName != "" {
94 | installed = true
95 | } else {
96 | log.Println("No installation detected, starting install server")
97 | srv := startInstallServer()
98 |
99 | // TODO there might be a bug here where we might have multiple instances waiting for installed to be true which causes an infinite loop
100 | // TODO make this use a channel instead of a loop to wait for install to finish
101 | for !installed {
102 | // Pause for 100 ms, this was causing high cpu load without this here
103 | time.Sleep(time.Millisecond * 100)
104 | // Keep running install server until installed is finished
105 | }
106 |
107 | if err := srv.Shutdown(context.TODO()); err != nil {
108 | panic(err) // failure/timeout shutting down the server gracefully
109 | }
110 | log.Println("Install complete, restarting server")
111 | // settingsInit() calls itself after install to reload settings
112 | settingsInit()
113 | }
114 |
115 | }
116 |
117 | // startInstallServer starts a http listener and presents the installation page to anyone visiting the port on the host
118 | func startInstallServer() *http.Server {
119 | r := mux.NewRouter()
120 | n := negroni.Classic()
121 |
122 | beuboTemplateRenderer := template.BeuboTemplateRenderer{
123 | ReloadTemplates: true,
124 | CurrentTheme: "install",
125 | }
126 |
127 | beuboTemplateRenderer.Init()
128 |
129 | beuboRouter := &routes.BeuboRouter{
130 | Renderer: &beuboTemplateRenderer,
131 | }
132 |
133 | r.NotFoundHandler = http.HandlerFunc(beuboRouter.NotFoundHandler)
134 |
135 | log.Println("Registering themes...")
136 |
137 | r = registerStaticFiles(r)
138 |
139 | log.Println("Registering routes...")
140 |
141 | r.HandleFunc("/", Install)
142 |
143 | n.UseHandler(r)
144 |
145 | srv := &http.Server{Addr: fmt.Sprintf(":%d", port), Handler: n}
146 |
147 | log.Println("HTTP Server listening on:", port)
148 | go func() {
149 | // returns ErrServerClosed on graceful close
150 | if err := srv.ListenAndServe(); err != http.ErrServerClosed {
151 | // NOTE: there is a chance that next line won't have time to run,
152 | // as main() doesn't wait for this goroutine to stop. don't use
153 | // code with race conditions like these for production. see post
154 | // comments below on more discussion on how to handle this.
155 | log.Fatalf("ListenAndServe(): %s", err)
156 | }
157 | log.Println("Server stopped")
158 | }()
159 |
160 | // returning reference so caller can call Shutdown()
161 | return srv
162 | }
163 |
164 | // Install handles installation requests and presents the install page
165 | func Install(w http.ResponseWriter, r *http.Request) {
166 |
167 | beuboTemplateRenderer := template.BeuboTemplateRenderer{
168 | ReloadTemplates: true,
169 | CurrentTheme: "install",
170 | }
171 |
172 | beuboTemplateRenderer.Init()
173 |
174 | pageData := structs.PageData{
175 | Template: "finished",
176 | Title: "Install",
177 | }
178 |
179 | formKey := "form"
180 | dbhostKey := "dbhost"
181 | dbnameKey := "dbname"
182 | dbuserKey := "dbuser"
183 | dbpasswordKey := "dbpassword"
184 | dbdriverKey := "dbdriver"
185 | usernameKey := "username"
186 | passwordKey := "password"
187 |
188 | if failures == nil {
189 | failures = make(map[string]map[string]string)
190 | }
191 |
192 | extra := make(map[string]map[string]string)
193 |
194 | if r.Method == http.MethodPost {
195 | extra[formKey] = make(map[string]string)
196 | err := r.ParseForm()
197 | if err != nil {
198 | utility.ErrorHandler(err, false)
199 | }
200 |
201 | extra[formKey][dbhostKey] = r.PostFormValue(dbhostKey)
202 | extra[formKey][dbnameKey] = r.PostFormValue(dbnameKey)
203 | extra[formKey][dbuserKey] = r.PostFormValue(dbuserKey)
204 | extra[formKey][dbpasswordKey] = r.PostFormValue(dbpasswordKey)
205 | extra[formKey][dbdriverKey] = r.PostFormValue(dbdriverKey)
206 | extra[formKey][usernameKey] = r.PostFormValue(usernameKey)
207 | extra[formKey][passwordKey] = r.PostFormValue(passwordKey)
208 |
209 | token, err := utility.GenerateToken(30)
210 | if err != nil {
211 | panic(err)
212 | }
213 |
214 | failures[token] = extra[formKey]
215 |
216 | utility.SetFlash(w, "token", []byte(token))
217 |
218 | if len(extra[formKey][usernameKey]) < 8 || len(extra[formKey][passwordKey]) < 8 {
219 | err = errors.New("username and password must be filled with a minimum of 8 characters")
220 | utility.SetFlash(w, "error", []byte(err.Error()))
221 | // Redirect back with error
222 | w.Header().Add("Location", "/")
223 | w.WriteHeader(302)
224 | return
225 | }
226 |
227 | newLogger := logger.New(
228 | log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
229 | logger.Config{
230 | SlowThreshold: time.Second, // Slow SQL threshold
231 | LogLevel: logger.Info, // Log level
232 | Colorful: true,
233 | },
234 | )
235 | config := gorm.Config{
236 | Logger: newLogger,
237 | }
238 | if extra[formKey][dbdriverKey] == "sqlite3" {
239 | config = gorm.Config{
240 | DisableForeignKeyConstraintWhenMigrating: true,
241 | Logger: newLogger,
242 | }
243 | }
244 |
245 | dialector := getDialector(extra[formKey][dbuserKey], extra[formKey][dbpasswordKey], extra[formKey][dbhostKey], databasePort, extra[formKey][dbnameKey], extra[formKey][dbdriverKey])
246 | _, err = gorm.Open(dialector, &config)
247 | if err != nil {
248 | utility.SetFlash(w, "error", []byte(err.Error()))
249 | // Redirect back with error
250 | w.Header().Add("Location", "/")
251 | w.WriteHeader(302)
252 | return
253 | }
254 |
255 | log.Println("no error, install done")
256 | writeEnv("", "", extra[formKey][dbhostKey], extra[formKey][dbnameKey], extra[formKey][dbuserKey], extra[formKey][dbpasswordKey], extra[formKey][dbdriverKey])
257 | beuboTemplateRenderer.RenderHTMLPage(w, r, pageData)
258 | currentTheme = "default"
259 | prepareSeed(extra[formKey][usernameKey], extra[formKey][passwordKey])
260 | databaseName = extra[formKey][dbnameKey]
261 | installed = true
262 | return
263 | }
264 |
265 | menuItems := []page.MenuItem{
266 | {Text: "Home", URI: "/"},
267 | }
268 |
269 | menus := []page.Menu{menu.DefaultMenu{
270 | Items: menuItems,
271 | Identifier: "header",
272 | T: beuboTemplateRenderer.T,
273 | }}
274 |
275 | extra = make(map[string]map[string]string)
276 | token, err := utility.GetFlash(w, r, "token")
277 | if err == nil {
278 | extra[formKey] = make(map[string]string)
279 | extra[formKey] = failures[string(token)]
280 | failures[string(token)] = nil
281 | }
282 | pageData = structs.PageData{
283 | Theme: "install",
284 | Template: "page",
285 | Title: "Install",
286 | Extra: extra,
287 | Menus: menus,
288 | }
289 | beuboTemplateRenderer.RenderHTMLPage(w, r, pageData)
290 | return
291 |
292 | }
293 |
294 | // setSetting returns the key value if it is set and otherwise falls back to return the variable value
295 | func setSetting(key string, variable string) string {
296 | if key != "" {
297 | variable = key
298 | }
299 | return variable
300 | }
301 |
302 | func setBoolSetting(key string, variable bool) bool {
303 | if key == "true" {
304 | return true
305 | } else if key == "false" {
306 | return false
307 | }
308 | return variable
309 | }
310 |
311 | // writeEnv writes environmental variables to an .env file
312 | func writeEnv(assetDir string, theme string, dbHost string, dbName string, dbUser string, dbPassword string, dbDriver string) {
313 | envContent := []byte("ASSETS_DIR=" + assetDir + "\nTHEME=" + theme + "\n\nDB_DRIVER=" + dbDriver + "\nDB_HOST=" + dbHost + "\nDB_NAME=" + dbName + "\nDB_USER=" + dbUser + "\nDB_PASSWORD=" + dbPassword + "\nSESSION_KEY=" + hex.EncodeToString(sessionKey))
314 | // TODO allow users to specify folder or even config filename, maybe beuboConfig
315 | err := ioutil.WriteFile(".env", envContent, 0600) // TODO allow user to change permissions here?
316 |
317 | // We panic if we can not write env
318 | utility.ErrorHandler(err, false)
319 | }
320 |
--------------------------------------------------------------------------------
/themes/README.md:
--------------------------------------------------------------------------------
1 | # Themes
2 |
3 | This is the themes directory. Beubo will look here for any static files that are needed when loading
4 | pages. I have included third party files directly in the repository and have opted not to use a package
5 | manager like npm because I feel that it's simpler and easier to understand.
6 |
7 | The default theme for Beubo can be found here: [https://github.com/uberswe/beubo-default](https://github.com/uberswe/beubo-default)
8 |
9 | Any third party libraries and files will have a license file or show a license at the top of the file.
10 |
11 | ## Best practice
12 |
13 | This is a general rule that I like to stick to. Themes should aim to be less than 50kb in size
14 | per page including css and any js but not counting images. Images should be as small as possible
15 | (50kb or less) without losing too much quality.
--------------------------------------------------------------------------------