mirror of
https://github.com/pushbits/server.git
synced 2025-05-12 16:37:02 +02:00
Initialize repository
This commit is contained in:
commit
1d758fcfd0
28 changed files with 1107 additions and 0 deletions
18
.editorconfig
Normal file
18
.editorconfig
Normal file
|
@ -0,0 +1,18 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = tab
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
*.db
|
||||
*.yml
|
||||
debugging/
|
||||
pushbits
|
||||
|
||||
### Go
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
7
LICENSE
Normal file
7
LICENSE
Normal file
|
@ -0,0 +1,7 @@
|
|||
ISC License (ISC)
|
||||
|
||||
Copyright 2020 eikendev
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
15
Makefile
Normal file
15
Makefile
Normal file
|
@ -0,0 +1,15 @@
|
|||
.PHONY: test
|
||||
test:
|
||||
stdout=$$(gofmt -l . 2>&1); \
|
||||
if [ "$$stdout" ]; then \
|
||||
exit 1; \
|
||||
fi
|
||||
go test -v -cover ./...
|
||||
stdout=$$(golint ./... 2>&1); \
|
||||
if [ "$$stdout" ]; then \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: dependencies
|
||||
dependencies:
|
||||
go get -u golang.org/x/lint/golint
|
30
README.md
Normal file
30
README.md
Normal file
|
@ -0,0 +1,30 @@
|
|||
## About
|
||||
|
||||
PushBits is a relay server for push notifications.
|
||||
It enables your services to send notifications via a simple web API, and delivers them to you through various messaging services.
|
||||
|
||||
The vision is to have compatibility with [Gotify](https://gotify.net/) on the sending side, while on the receiving side established services are used.
|
||||
This has the advantages that
|
||||
- plugins written for Gotify and
|
||||
- clients written for all major platforms can be reused.
|
||||
|
||||
For now, only the [Matrix protocol](https://matrix.org/) is supported, but support for different services like [Telegram](https://telegram.org/) could be added in the future.
|
||||
I am myself experimenting with Matrix currently because I like the idea of a federated, synchronized but still end-to-end encrypted protocol.
|
||||
|
||||
The idea for this software and most parts of the initial source are heavily inspired by [Gotify](https://gotify.net/).
|
||||
Many thanks to [jmattheis](https://jmattheis.de/) for his well-structured code.
|
||||
|
||||
## Usage
|
||||
|
||||
PushBits is meant to be self-hosted.
|
||||
You are advised to install PushBits behind a reverse proxy and enable TLS.
|
||||
|
||||
At the moment, there is no front-end implemented.
|
||||
New users and applications need to be created via the API.
|
||||
Details will be made available once the interface is more stable.
|
||||
|
||||
## Development
|
||||
|
||||
PushBits is currently in alpha stage.
|
||||
The API is neither stable, nor is provided functionality guaranteed to work.
|
||||
Stay tuned! 😉
|
63
api/application.go
Normal file
63
api/application.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/eikendev/pushbits/authentication"
|
||||
"github.com/eikendev/pushbits/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// The ApplicationDatabase interface for encapsulating database access.
|
||||
type ApplicationDatabase interface {
|
||||
CreateApplication(application *model.Application) error
|
||||
GetApplicationByToken(token string) (*model.Application, error)
|
||||
}
|
||||
|
||||
// The ApplicationDispatcher interface for relaying notifications.
|
||||
type ApplicationDispatcher interface {
|
||||
RegisterApplication(name, user string) (string, error)
|
||||
}
|
||||
|
||||
// ApplicationHandler holds information for processing requests about applications.
|
||||
type ApplicationHandler struct {
|
||||
DB ApplicationDatabase
|
||||
Dispatcher ApplicationDispatcher
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) applicationExists(token string) bool {
|
||||
application, _ := h.DB.GetApplicationByToken(token)
|
||||
return application != nil
|
||||
}
|
||||
|
||||
// CreateApplication is used to create a new user.
|
||||
func (h *ApplicationHandler) CreateApplication(ctx *gin.Context) {
|
||||
application := model.Application{}
|
||||
|
||||
if success := successOrAbort(ctx, http.StatusBadRequest, ctx.Bind(&application)); !success {
|
||||
return
|
||||
}
|
||||
|
||||
user := authentication.GetUser(ctx)
|
||||
|
||||
application.Token = authentication.GenerateNotExistingToken(authentication.GenerateApplicationToken, h.applicationExists)
|
||||
application.UserID = user.ID
|
||||
|
||||
log.Printf("User %s will receive notifications for application %s.\n", user.Name, application.Name)
|
||||
|
||||
matrixid, err := h.Dispatcher.RegisterApplication(application.Name, user.MatrixID)
|
||||
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
|
||||
return
|
||||
}
|
||||
|
||||
application.MatrixID = matrixid
|
||||
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, h.DB.CreateApplication(&application)); !success {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &application)
|
||||
}
|
53
api/notification.go
Normal file
53
api/notification.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/eikendev/pushbits/authentication"
|
||||
"github.com/eikendev/pushbits/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// The NotificationDatabase interface for encapsulating database access.
|
||||
type NotificationDatabase interface {
|
||||
}
|
||||
|
||||
// The NotificationDispatcher interface for relaying notifications.
|
||||
type NotificationDispatcher interface {
|
||||
SendNotification(a *model.Application, n *model.Notification) error
|
||||
}
|
||||
|
||||
// NotificationHandler holds information for processing requests about notifications.
|
||||
type NotificationHandler struct {
|
||||
DB NotificationDatabase
|
||||
Dispatcher NotificationDispatcher
|
||||
}
|
||||
|
||||
// CreateNotification is used to create a new notification for a user.
|
||||
func (h *NotificationHandler) CreateNotification(ctx *gin.Context) {
|
||||
notification := model.Notification{}
|
||||
|
||||
if success := successOrAbort(ctx, http.StatusBadRequest, ctx.Bind(¬ification)); !success {
|
||||
return
|
||||
}
|
||||
|
||||
application := authentication.GetApplication(ctx)
|
||||
log.Printf("Sending notification for application %s.\n", application.Name)
|
||||
|
||||
notification.ID = 0
|
||||
notification.ApplicationID = application.ID
|
||||
if strings.TrimSpace(notification.Title) == "" {
|
||||
notification.Title = application.Name
|
||||
}
|
||||
notification.Date = time.Now()
|
||||
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, h.Dispatcher.SendNotification(application, ¬ification)); !success {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, ¬ification)
|
||||
}
|
48
api/user.go
Normal file
48
api/user.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/eikendev/pushbits/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// The UserDatabase interface for encapsulating database access.
|
||||
type UserDatabase interface {
|
||||
CreateUser(user *model.User) error
|
||||
GetUserByName(name string) (*model.User, error)
|
||||
}
|
||||
|
||||
// UserHandler holds information for processing requests about users.
|
||||
type UserHandler struct {
|
||||
DB UserDatabase
|
||||
}
|
||||
|
||||
func (h *UserHandler) userExists(name string) bool {
|
||||
user, _ := h.DB.GetUserByName(name)
|
||||
return user != nil
|
||||
}
|
||||
|
||||
// CreateUser creates a new user.
|
||||
func (h *UserHandler) CreateUser(ctx *gin.Context) {
|
||||
externalUser := model.ExternalUserWithCredentials{}
|
||||
|
||||
if success := successOrAbort(ctx, http.StatusBadRequest, ctx.Bind(&externalUser)); !success {
|
||||
return
|
||||
}
|
||||
|
||||
user := externalUser.IntoInternalUser()
|
||||
|
||||
if h.userExists(user.Name) {
|
||||
ctx.AbortWithError(http.StatusBadRequest, errors.New("username already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, h.DB.CreateUser(user)); !success {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, user.IntoExternalUser())
|
||||
}
|
13
api/util.go
Normal file
13
api/util.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func successOrAbort(ctx *gin.Context, code int, err error) bool {
|
||||
if err != nil {
|
||||
ctx.AbortWithError(code, err)
|
||||
}
|
||||
|
||||
return err == nil
|
||||
}
|
53
app.go
Normal file
53
app.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/eikendev/pushbits/configuration"
|
||||
"github.com/eikendev/pushbits/database"
|
||||
"github.com/eikendev/pushbits/dispatcher"
|
||||
"github.com/eikendev/pushbits/router"
|
||||
"github.com/eikendev/pushbits/runner"
|
||||
)
|
||||
|
||||
func setupCleanup(db *database.Database, dp *dispatcher.Dispatcher) {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-c
|
||||
dp.Close()
|
||||
db.Close()
|
||||
os.Exit(1)
|
||||
}()
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.Println("Starting PushBits.")
|
||||
|
||||
c := configuration.Get()
|
||||
|
||||
db, err := database.Create(c.Database.Dialect, c.Database.Connection)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err := db.Populate(c.Admin.Name, c.Admin.Password, c.Admin.MatrixID); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
dp, err := dispatcher.Create(db, c.Matrix.Homeserver, c.Matrix.Username, c.Matrix.Password)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer dp.Close()
|
||||
|
||||
setupCleanup(db, dp)
|
||||
|
||||
engine := router.Create(db, dp)
|
||||
runner.Run(engine)
|
||||
}
|
106
authentication/authentication.go
Normal file
106
authentication/authentication.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/eikendev/pushbits/authentication/credentials"
|
||||
"github.com/eikendev/pushbits/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
headerName = "X-Gotify-Key"
|
||||
)
|
||||
|
||||
// The Database interface for encapsulating database access.
|
||||
type Database interface {
|
||||
GetApplicationByToken(token string) (*model.Application, error)
|
||||
GetUserByName(name string) (*model.User, error)
|
||||
}
|
||||
|
||||
// Authenticator is the provider for authentication middleware.
|
||||
type Authenticator struct {
|
||||
DB Database
|
||||
}
|
||||
|
||||
type hasUserProperty func(user *model.User) bool
|
||||
|
||||
func (a *Authenticator) userFromBasicAuth(ctx *gin.Context) (*model.User, error) {
|
||||
if name, password, ok := ctx.Request.BasicAuth(); ok {
|
||||
if user, err := a.DB.GetUserByName(name); err != nil {
|
||||
return nil, err
|
||||
} else if user != nil && credentials.ComparePassword(user.PasswordHash, []byte(password)) {
|
||||
return user, nil
|
||||
} else {
|
||||
return nil, errors.New("credentials were invalid")
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("no credentials were supplied")
|
||||
}
|
||||
|
||||
func (a *Authenticator) requireUserProperty(has hasUserProperty) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
user, err := a.userFromBasicAuth(ctx)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusForbidden, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !has(user) {
|
||||
ctx.AbortWithError(http.StatusForbidden, errors.New("authentication failed"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Set("user", user)
|
||||
}
|
||||
}
|
||||
|
||||
// RequireUser returns a Gin middleware which requires valid user credentials to be supplied with the request.
|
||||
func (a *Authenticator) RequireUser() gin.HandlerFunc {
|
||||
return a.requireUserProperty(func(user *model.User) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// RequireAdmin returns a Gin middleware which requires valid admin credentials to be supplied with the request.
|
||||
func (a *Authenticator) RequireAdmin() gin.HandlerFunc {
|
||||
return a.requireUserProperty(func(user *model.User) bool {
|
||||
return user.IsAdmin
|
||||
})
|
||||
}
|
||||
|
||||
func (a *Authenticator) tokenFromQueryOrHeader(ctx *gin.Context) string {
|
||||
if token := a.tokenFromQuery(ctx); token != "" {
|
||||
return token
|
||||
} else if token := a.tokenFromHeader(ctx); token != "" {
|
||||
return token
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *Authenticator) tokenFromQuery(ctx *gin.Context) string {
|
||||
return ctx.Request.URL.Query().Get("token")
|
||||
}
|
||||
|
||||
func (a *Authenticator) tokenFromHeader(ctx *gin.Context) string {
|
||||
return ctx.Request.Header.Get(headerName)
|
||||
}
|
||||
|
||||
// RequireApplicationToken returns a Gin middleware which requires an application token to be supplied with the request.
|
||||
func (a *Authenticator) RequireApplicationToken() gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
token := a.tokenFromQueryOrHeader(ctx)
|
||||
|
||||
app, err := a.DB.GetApplicationByToken(token)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusForbidden, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Set("app", app)
|
||||
}
|
||||
}
|
30
authentication/context.go
Normal file
30
authentication/context.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/eikendev/pushbits/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetApplication returns the application which was previously registered by the authentication middleware.
|
||||
func GetApplication(ctx *gin.Context) *model.Application {
|
||||
app, ok := ctx.MustGet("app").(*model.Application)
|
||||
if app == nil || !ok {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, errors.New("an error occured while retrieving application from context"))
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
// GetUser returns the user which was previously registered by the authentication middleware.
|
||||
func GetUser(ctx *gin.Context) *model.User {
|
||||
user, ok := ctx.MustGet("user").(*model.User)
|
||||
if user == nil || !ok {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, errors.New("an error occured while retrieving user from context"))
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
21
authentication/credentials/credentials.go
Normal file
21
authentication/credentials/credentials.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package credentials
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
// CreatePassword returns a hashed version of the given password.
|
||||
// TODO: Make strength configurable.
|
||||
func CreatePassword(pw string) []byte {
|
||||
strength := 12
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(pw), strength)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return hashedPassword
|
||||
}
|
||||
|
||||
// ComparePassword compares a hashed password with its possible plaintext equivalent.
|
||||
func ComparePassword(hashedPassword, password []byte) bool {
|
||||
return bcrypt.CompareHashAndPassword(hashedPassword, password) == nil
|
||||
}
|
54
authentication/token.go
Normal file
54
authentication/token.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
var (
|
||||
tokenCharacters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
randomTokenLength = 64
|
||||
applicationPrefix = "A"
|
||||
)
|
||||
|
||||
func randIntn(n int) int {
|
||||
max := big.NewInt(int64(n))
|
||||
|
||||
res, err := rand.Int(rand.Reader, max)
|
||||
if err != nil {
|
||||
panic("random source is not available")
|
||||
}
|
||||
|
||||
return int(res.Int64())
|
||||
}
|
||||
|
||||
// GenerateNotExistingToken receives a token generation function and a function to check whether the token exists, returns a unique token.
|
||||
func GenerateNotExistingToken(generateToken func() string, tokenExists func(token string) bool) string {
|
||||
for {
|
||||
token := generateToken()
|
||||
|
||||
if !tokenExists(token) {
|
||||
return token
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateRandomString(length int) string {
|
||||
res := make([]byte, length)
|
||||
|
||||
for i := range res {
|
||||
index := randIntn(len(tokenCharacters))
|
||||
res[i] = tokenCharacters[index]
|
||||
}
|
||||
|
||||
return string(res)
|
||||
}
|
||||
|
||||
func generateRandomToken(prefix string) string {
|
||||
return prefix + generateRandomString(randomTokenLength)
|
||||
}
|
||||
|
||||
// GenerateApplicationToken generates a token for an application.
|
||||
func GenerateApplicationToken() string {
|
||||
return generateRandomToken(applicationPrefix)
|
||||
}
|
43
configuration/configuration.go
Normal file
43
configuration/configuration.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package configuration
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/configor"
|
||||
)
|
||||
|
||||
// Configuration holds values that can be configured by the user.
|
||||
type Configuration struct {
|
||||
Database struct {
|
||||
Dialect string `default:"sqlite3"`
|
||||
Connection string `default:"pushbits.db"`
|
||||
}
|
||||
Admin struct {
|
||||
Name string `default:"admin"`
|
||||
Password string `default:"admin"`
|
||||
MatrixID string `required:"true"`
|
||||
}
|
||||
Matrix struct {
|
||||
Homeserver string `default:"https://matrix.org"`
|
||||
Username string `required:"true"`
|
||||
Password string `required:"true"`
|
||||
}
|
||||
}
|
||||
|
||||
func configFiles() []string {
|
||||
return []string{"config.yml"}
|
||||
}
|
||||
|
||||
// Get returns the configuration extracted from env variables or config file.
|
||||
func Get() *Configuration {
|
||||
config := new(Configuration)
|
||||
|
||||
err := configor.New(&configor.Config{
|
||||
Environment: "production",
|
||||
ENVPrefix: "PUSHBITS",
|
||||
ErrorOnUnmatchedKeys: true,
|
||||
}).Load(config, configFiles()...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
31
database/application.go
Normal file
31
database/application.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/eikendev/pushbits/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CreateApplication creates an application.
|
||||
func (d *Database) CreateApplication(application *model.Application) error {
|
||||
return d.gormdb.Create(application).Error
|
||||
}
|
||||
|
||||
// UpdateApplication updates an application.
|
||||
func (d *Database) UpdateApplication(app *model.Application) error {
|
||||
return d.gormdb.Save(app).Error
|
||||
}
|
||||
|
||||
// GetApplicationByToken returns the application for the given token or nil.
|
||||
func (d *Database) GetApplicationByToken(token string) (*model.Application, error) {
|
||||
app := new(model.Application)
|
||||
err := d.gormdb.Where("token = ?", token).First(app).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return app, err
|
||||
}
|
101
database/database.go
Normal file
101
database/database.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/eikendev/pushbits/authentication/credentials"
|
||||
"github.com/eikendev/pushbits/model"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Database holds information for the database connection.
|
||||
type Database struct {
|
||||
gormdb *gorm.DB
|
||||
sqldb *sql.DB
|
||||
}
|
||||
|
||||
func createFileDir(file string) {
|
||||
if _, err := os.Stat(filepath.Dir(file)); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(filepath.Dir(file), 0775); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create instanciates a database connection.
|
||||
func Create(dialect, connection string) (*Database, error) {
|
||||
log.Println("Setting up database connection.")
|
||||
|
||||
maxOpenConns := 5
|
||||
|
||||
var db *gorm.DB
|
||||
var err error
|
||||
|
||||
switch dialect {
|
||||
case "sqlite3":
|
||||
createFileDir(connection)
|
||||
maxOpenConns = 1
|
||||
db, err = gorm.Open(sqlite.Open(connection), &gorm.Config{})
|
||||
case "mysql":
|
||||
db, err = gorm.Open(mysql.Open(connection), &gorm.Config{})
|
||||
default:
|
||||
message := "Database dialect is not supported"
|
||||
return nil, errors.New(message)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sql, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sql.SetMaxOpenConns(maxOpenConns)
|
||||
|
||||
if dialect == "mysql" {
|
||||
sql.SetConnMaxLifetime(9 * time.Minute)
|
||||
}
|
||||
|
||||
db.AutoMigrate(&model.User{}, &model.Application{})
|
||||
|
||||
return &Database{gormdb: db, sqldb: sql}, nil
|
||||
}
|
||||
|
||||
// Close closes the database connection.
|
||||
func (d *Database) Close() {
|
||||
d.sqldb.Close()
|
||||
}
|
||||
|
||||
// Populate fills the database with initial information like the admin user.
|
||||
func (d *Database) Populate(name, password, matrixID string) error {
|
||||
user := new(model.User)
|
||||
query := d.gormdb.Where("name = ?", name).First(user)
|
||||
|
||||
if errors.Is(query.Error, gorm.ErrRecordNotFound) {
|
||||
user := model.NewUser(name, password, true, matrixID)
|
||||
|
||||
if err := d.gormdb.Create(&user).Error; err != nil {
|
||||
return errors.New("user cannot be created")
|
||||
}
|
||||
} else {
|
||||
log.Printf("Admin user %s already exists.\n", name)
|
||||
|
||||
user.PasswordHash = credentials.CreatePassword(password)
|
||||
user.IsAdmin = true
|
||||
user.MatrixID = matrixID
|
||||
|
||||
d.gormdb.Save(&user)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
26
database/user.go
Normal file
26
database/user.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/eikendev/pushbits/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CreateUser creates a user.
|
||||
func (d *Database) CreateUser(user *model.User) error {
|
||||
return d.gormdb.Create(user).Error
|
||||
}
|
||||
|
||||
// GetUserByName returns the user by the given name or nil.
|
||||
func (d *Database) GetUserByName(name string) (*model.User, error) {
|
||||
user := new(model.User)
|
||||
err := d.gormdb.Where("name = ?", name).First(user).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, err
|
||||
}
|
29
dispatcher/application.go
Normal file
29
dispatcher/application.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package dispatcher
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/matrix-org/gomatrix"
|
||||
)
|
||||
|
||||
// RegisterApplication creates a new channel for the application.
|
||||
func (d *Dispatcher) RegisterApplication(name, user string) (string, error) {
|
||||
log.Printf("Registering application %s, notifications will be relayed to user %s.\n", name, user)
|
||||
|
||||
response, err := d.client.CreateRoom(&gomatrix.ReqCreateRoom{
|
||||
Visibility: "private",
|
||||
Name: name,
|
||||
Invite: []string{user},
|
||||
Preset: "private_chat",
|
||||
IsDirect: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Printf("Application %s is now relayed to room with ID %s.\n", name, response.RoomID)
|
||||
|
||||
return response.RoomID, err
|
||||
}
|
58
dispatcher/dispatcher.go
Normal file
58
dispatcher/dispatcher.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package dispatcher
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/eikendev/pushbits/model"
|
||||
|
||||
"github.com/matrix-org/gomatrix"
|
||||
)
|
||||
|
||||
var (
|
||||
loginType = "m.login.password"
|
||||
)
|
||||
|
||||
// The Database interface for encapsulating database access.
|
||||
type Database interface {
|
||||
UpdateApplication(application *model.Application) error
|
||||
}
|
||||
|
||||
// Dispatcher holds information for sending notifications to clients.
|
||||
type Dispatcher struct {
|
||||
db Database
|
||||
client *gomatrix.Client
|
||||
}
|
||||
|
||||
// Create instanciates a dispatcher connection.
|
||||
// TODO: Call JoinedRooms() to validate room mappings on startup.
|
||||
// TODO: ForgetRoom() for unused rooms.
|
||||
// TODO: InviteUser() if the user is no longer in the room.
|
||||
func Create(db Database, homeserver, username, password string) (*Dispatcher, error) {
|
||||
log.Println("Setting up dispatcher.")
|
||||
|
||||
client, err := gomatrix.NewClient(homeserver, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := client.Login(&gomatrix.ReqLogin{
|
||||
Type: loginType,
|
||||
User: username,
|
||||
Password: password,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.SetCredentials(response.UserID, response.AccessToken)
|
||||
|
||||
return &Dispatcher{client: client}, nil
|
||||
}
|
||||
|
||||
// Close closes the dispatcher connection.
|
||||
func (d *Dispatcher) Close() {
|
||||
log.Printf("Logging out.\n")
|
||||
|
||||
d.client.Logout()
|
||||
d.client.ClearCredentials()
|
||||
}
|
19
dispatcher/notification.go
Normal file
19
dispatcher/notification.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package dispatcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/eikendev/pushbits/model"
|
||||
)
|
||||
|
||||
// SendNotification sends a notification to the specified user.
|
||||
func (d *Dispatcher) SendNotification(a *model.Application, n *model.Notification) error {
|
||||
log.Printf("Sending notification to room %s.\n", a.MatrixID)
|
||||
|
||||
text := fmt.Sprintf("%s\n\n%s", n.Title, n.Message)
|
||||
|
||||
_, err := d.client.SendText(a.MatrixID, text)
|
||||
|
||||
return err
|
||||
}
|
16
go.mod
Normal file
16
go.mod
Normal file
|
@ -0,0 +1,16 @@
|
|||
module github.com/eikendev/pushbits
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/location v0.0.2
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
github.com/jinzhu/configor v1.2.0
|
||||
github.com/matrix-org/gomatrix v0.0.0-20200501121722-e5578b12c752
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
|
||||
golang.org/x/tools v0.0.0-20200724172932-b5fc9d354d99 // indirect
|
||||
gorm.io/driver/mysql v0.3.1
|
||||
gorm.io/driver/sqlite v1.0.8
|
||||
gorm.io/gorm v0.2.24
|
||||
)
|
100
go.sum
Normal file
100
go.sum
Normal file
|
@ -0,0 +1,100 @@
|
|||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gin-contrib/location v0.0.2 h1:QZKh1+K/LLR4KG/61eIO3b7MLuKi8tytQhV6texLgP4=
|
||||
github.com/gin-contrib/location v0.0.2/go.mod h1:NGoidiRlf0BlA/VKSVp+g3cuSMeTmip/63PhEjRhUAc=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/jinzhu/configor v1.2.0 h1:u78Jsrxw2+3sGbGMgpY64ObKU4xWCNmNRJIjGVqxYQA=
|
||||
github.com/jinzhu/configor v1.2.0/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
|
||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/matrix-org/gomatrix v0.0.0-20200501121722-e5578b12c752 h1:xYU2vdY5uh7QX4RZqeuKpmv0ffBEGYHhgu4Mmm3RVnc=
|
||||
github.com/matrix-org/gomatrix v0.0.0-20200501121722-e5578b12c752/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 h1:EBZoQjiKKPaLbPrbpssUfuHtwM6KV/vb4U85g/cigFY=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200724172932-b5fc9d354d99 h1:OHn441rq5CeM5r1xJ0OmY7lfdTvnedi6k+vQiI7G9b8=
|
||||
golang.org/x/tools v0.0.0-20200724172932-b5fc9d354d99/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gorm.io/driver/mysql v0.3.1 h1:yvUT7Q0I3B9EHJ67NSp6cHbVwcdDHhVUsDAUiFFxRk0=
|
||||
gorm.io/driver/mysql v0.3.1/go.mod h1:A7H1JD9dKdcjeUTpTuWKEC+E1a74qzW7/zaXqKaTbfM=
|
||||
gorm.io/driver/sqlite v1.0.8 h1:omllgSb7/eh9D6lGvLZOdU1ZElxdXuO3dn3Rk+dQxUE=
|
||||
gorm.io/driver/sqlite v1.0.8/go.mod h1:xkm8/CEmA3yc4zRd0pdCqm43BjO8Hm6avfTpxWb/7c4=
|
||||
gorm.io/gorm v0.2.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v0.2.19/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v0.2.24 h1:NKjfopoXim5xAHYkt/uN0n7ZNmyoXZ9FEZUX4ijzXlI=
|
||||
gorm.io/gorm v0.2.24/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
10
model/application.go
Normal file
10
model/application.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package model
|
||||
|
||||
// Application holds information like the name, the token, and the associated user of an application.
|
||||
type Application struct {
|
||||
ID uint `gorm:"AUTO_INCREMENT;primary_key" json:"id"`
|
||||
Token string `gorm:"type:string;size:64;unique" json:"token"`
|
||||
UserID uint `json:"-"`
|
||||
Name string `gorm:"type:string" form:"name" query:"name" json:"name" binding:"required"`
|
||||
MatrixID string `gorm:"type:string"`
|
||||
}
|
16
model/notification.go
Normal file
16
model/notification.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Notification holds information like the message, the title, and the priority of a notification.
|
||||
type Notification struct {
|
||||
ID uint `json:"id"`
|
||||
ApplicationID uint `json:"appid"`
|
||||
Message string `json:"message" form:"message" query:"message" binding:"required"`
|
||||
Title string `json:"title" form:"title" query:"title"`
|
||||
Priority int `json:"priority" form:"priority" query:"priority"`
|
||||
Extras map[string]interface{} `json:"extras,omitempty" form:"-" query:"-"`
|
||||
Date time.Time `json:"date"`
|
||||
}
|
70
model/user.go
Normal file
70
model/user.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/eikendev/pushbits/authentication/credentials"
|
||||
)
|
||||
|
||||
// User holds information like the name, the secret, and the applications of a user.
|
||||
type User struct {
|
||||
ID uint `gorm:"AUTO_INCREMENT;primary_key"`
|
||||
Name string `gorm:"type:string;size:128;unique"`
|
||||
PasswordHash []byte
|
||||
IsAdmin bool
|
||||
MatrixID string `gorm:"type:string"`
|
||||
Applications []Application
|
||||
}
|
||||
|
||||
// ExternalUser represents a user for external purposes.
|
||||
type ExternalUser struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name" form:"name" query:"name" binding:"required"`
|
||||
IsAdmin bool `json:"is_admin" form:"is_admin" query:"is_admin"`
|
||||
MatrixID string `json:"matrix_id" form:"matrix_id" query:"matrix_id" binding:"required"`
|
||||
}
|
||||
|
||||
// UserCredentials holds information for authenticating a user.
|
||||
type UserCredentials struct {
|
||||
Password string `json:"password,omitempty" form:"password" query:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// ExternalUserWithCredentials represents a user for external purposes and includes the user's credentials in plaintext.
|
||||
type ExternalUserWithCredentials struct {
|
||||
ExternalUser
|
||||
UserCredentials
|
||||
}
|
||||
|
||||
// NewUser creates a new user.
|
||||
func NewUser(name, password string, isAdmin bool, matrixID string) *User {
|
||||
log.Printf("Creating user %s.\n", name)
|
||||
|
||||
user := User{
|
||||
Name: name,
|
||||
PasswordHash: credentials.CreatePassword(password),
|
||||
IsAdmin: isAdmin,
|
||||
MatrixID: matrixID,
|
||||
}
|
||||
|
||||
return &user
|
||||
}
|
||||
|
||||
// IntoInternalUser converts a ExternalUserWithCredentials into a User.
|
||||
func (u *ExternalUserWithCredentials) IntoInternalUser() *User {
|
||||
return &User{
|
||||
Name: u.Name,
|
||||
PasswordHash: credentials.CreatePassword(u.Password),
|
||||
IsAdmin: u.IsAdmin,
|
||||
MatrixID: u.MatrixID,
|
||||
}
|
||||
}
|
||||
|
||||
// IntoExternalUser converts a User into a ExternalUser.
|
||||
func (u *User) IntoExternalUser() *ExternalUser {
|
||||
return &ExternalUser{
|
||||
ID: u.ID,
|
||||
Name: u.Name,
|
||||
IsAdmin: u.IsAdmin,
|
||||
MatrixID: u.MatrixID,
|
||||
}
|
||||
}
|
45
router/router.go
Normal file
45
router/router.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/eikendev/pushbits/api"
|
||||
"github.com/eikendev/pushbits/authentication"
|
||||
"github.com/eikendev/pushbits/database"
|
||||
"github.com/eikendev/pushbits/dispatcher"
|
||||
|
||||
"github.com/gin-contrib/location"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Create a Gin engine and setup all routes.
|
||||
func Create(db *database.Database, dp *dispatcher.Dispatcher) *gin.Engine {
|
||||
log.Println("Setting up HTTP routes.")
|
||||
|
||||
auth := authentication.Authenticator{DB: db}
|
||||
|
||||
applicationHandler := api.ApplicationHandler{DB: db, Dispatcher: dp}
|
||||
notificationHandler := api.NotificationHandler{DB: db, Dispatcher: dp}
|
||||
userHandler := api.UserHandler{DB: db}
|
||||
|
||||
r := gin.Default()
|
||||
r.Use(location.Default())
|
||||
|
||||
applicationGroup := r.Group("/application")
|
||||
applicationGroup.Use(auth.RequireUser())
|
||||
{
|
||||
applicationGroup.POST("", applicationHandler.CreateApplication)
|
||||
//applicationGroup.DELETE("/:id", applicationHandler.DeleteApplication)
|
||||
}
|
||||
|
||||
r.POST("/message", auth.RequireApplicationToken(), notificationHandler.CreateNotification)
|
||||
|
||||
userGroup := r.Group("/user")
|
||||
userGroup.Use(auth.RequireAdmin())
|
||||
{
|
||||
userGroup.POST("", userHandler.CreateUser)
|
||||
//userGroup.DELETE("/:id", userHandler.DeleteUser)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
10
runner/runner.go
Normal file
10
runner/runner.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Run starts the Gin engine.
|
||||
func Run(engine *gin.Engine) {
|
||||
engine.Run(":8080")
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue