mirror of
https://github.com/pushbits/server.git
synced 2025-08-04 00:58:54 +02:00
Restructure project layout
This commit is contained in:
parent
a49db216d5
commit
9a4a096526
32 changed files with 35 additions and 35 deletions
192
internal/api/application.go
Normal file
192
internal/api/application.go
Normal file
|
@ -0,0 +1,192 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/pushbits/server/internal/authentication"
|
||||
"github.com/pushbits/server/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ApplicationHandler holds information for processing requests about applications.
|
||||
type ApplicationHandler struct {
|
||||
DB Database
|
||||
DP Dispatcher
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) applicationExists(token string) bool {
|
||||
application, _ := h.DB.GetApplicationByToken(token)
|
||||
return application != nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) registerApplication(ctx *gin.Context, a *model.Application, u *model.User) error {
|
||||
log.Printf("Registering application %s.\n", a.Name)
|
||||
|
||||
channelID, err := h.DP.RegisterApplication(a.ID, a.Name, a.Token, u.MatrixID)
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
|
||||
return err
|
||||
}
|
||||
|
||||
a.MatrixID = channelID
|
||||
h.DB.UpdateApplication(a)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) createApplication(ctx *gin.Context, name string, u *model.User) (*model.Application, error) {
|
||||
log.Printf("Creating application %s.\n", name)
|
||||
|
||||
application := model.Application{}
|
||||
application.Name = name
|
||||
application.Token = authentication.GenerateNotExistingToken(authentication.GenerateApplicationToken, h.applicationExists)
|
||||
application.UserID = u.ID
|
||||
|
||||
err := h.DB.CreateApplication(&application)
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := h.registerApplication(ctx, &application, u); err != nil {
|
||||
err := h.DB.DeleteApplication(&application)
|
||||
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
|
||||
log.Printf("Cannot delete application with ID %d.\n", application.ID)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &application, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) deleteApplication(ctx *gin.Context, a *model.Application, u *model.User) error {
|
||||
log.Printf("Deleting application %s (ID %d).\n", a.Name, a.ID)
|
||||
|
||||
err := h.DP.DeregisterApplication(a, u)
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.DB.DeleteApplication(a)
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) updateApplication(ctx *gin.Context, a *model.Application, updateApplication *model.UpdateApplication) error {
|
||||
log.Printf("Updating application %s.\n", a.Name)
|
||||
|
||||
if updateApplication.Name != nil {
|
||||
a.Name = *updateApplication.Name
|
||||
}
|
||||
|
||||
err := h.DB.UpdateApplication(a)
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateApplication creates an application.
|
||||
func (h *ApplicationHandler) CreateApplication(ctx *gin.Context) {
|
||||
var createApplication model.CreateApplication
|
||||
|
||||
if err := ctx.Bind(&createApplication); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
user := authentication.GetUser(ctx)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
application, err := h.createApplication(ctx, createApplication.Name, user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &application)
|
||||
}
|
||||
|
||||
// GetApplications returns all applications of the current user.
|
||||
func (h *ApplicationHandler) GetApplications(ctx *gin.Context) {
|
||||
user := authentication.GetUser(ctx)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
applications, err := h.DB.GetApplications(user)
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &applications)
|
||||
}
|
||||
|
||||
// GetApplication returns the application with the specified ID.
|
||||
func (h *ApplicationHandler) GetApplication(ctx *gin.Context) {
|
||||
application, err := getApplication(ctx, h.DB)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
user := authentication.GetUser(ctx)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if user.ID != application.UserID {
|
||||
err := errors.New("application belongs to another user")
|
||||
ctx.AbortWithError(http.StatusForbidden, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &application)
|
||||
}
|
||||
|
||||
// DeleteApplication deletes an application with a certain ID.
|
||||
func (h *ApplicationHandler) DeleteApplication(ctx *gin.Context) {
|
||||
application, err := getApplication(ctx, h.DB)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !isCurrentUser(ctx, application.UserID) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.deleteApplication(ctx, application, authentication.GetUser(ctx)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
// UpdateApplication updates an application with a certain ID.
|
||||
func (h *ApplicationHandler) UpdateApplication(ctx *gin.Context) {
|
||||
application, err := getApplication(ctx, h.DB)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !isCurrentUser(ctx, application.UserID) {
|
||||
return
|
||||
}
|
||||
|
||||
var updateApplication model.UpdateApplication
|
||||
if err := ctx.BindUri(&updateApplication); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.updateApplication(ctx, application, &updateApplication); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{})
|
||||
}
|
49
internal/api/context.go
Normal file
49
internal/api/context.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/pushbits/server/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func getID(ctx *gin.Context) (uint, error) {
|
||||
id, ok := ctx.MustGet("id").(uint)
|
||||
if !ok {
|
||||
err := errors.New("an error occured while retrieving ID from context")
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func getApplication(ctx *gin.Context, db Database) (*model.Application, error) {
|
||||
id, err := getID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
application, err := db.GetApplicationByID(id)
|
||||
if success := successOrAbort(ctx, http.StatusNotFound, err); !success {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return application, nil
|
||||
}
|
||||
|
||||
func getUser(ctx *gin.Context, db Database) (*model.User, error) {
|
||||
id, err := getID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := db.GetUserByID(id)
|
||||
if success := successOrAbort(ctx, http.StatusNotFound, err); !success {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
22
internal/api/health.go
Normal file
22
internal/api/health.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HealthHandler holds information for processing requests about the server's health.
|
||||
type HealthHandler struct {
|
||||
DB Database
|
||||
}
|
||||
|
||||
// Health returns the health status of the server.
|
||||
func (h *HealthHandler) Health(ctx *gin.Context) {
|
||||
if err := h.DB.Health(); err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{})
|
||||
}
|
36
internal/api/interfaces.go
Normal file
36
internal/api/interfaces.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/pushbits/server/internal/model"
|
||||
)
|
||||
|
||||
// The Database interface for encapsulating database access.
|
||||
type Database interface {
|
||||
Health() error
|
||||
|
||||
CreateApplication(application *model.Application) error
|
||||
DeleteApplication(application *model.Application) error
|
||||
GetApplicationByID(ID uint) (*model.Application, error)
|
||||
GetApplicationByToken(token string) (*model.Application, error)
|
||||
UpdateApplication(application *model.Application) error
|
||||
|
||||
AdminUserCount() (int64, error)
|
||||
CreateUser(user model.CreateUser) (*model.User, error)
|
||||
DeleteUser(user *model.User) error
|
||||
GetApplications(user *model.User) ([]model.Application, error)
|
||||
GetUserByID(ID uint) (*model.User, error)
|
||||
GetUserByName(name string) (*model.User, error)
|
||||
GetUsers() ([]model.User, error)
|
||||
UpdateUser(user *model.User) error
|
||||
}
|
||||
|
||||
// The Dispatcher interface for relaying notifications.
|
||||
type Dispatcher interface {
|
||||
RegisterApplication(id uint, name, token, user string) (string, error)
|
||||
DeregisterApplication(a *model.Application, u *model.User) error
|
||||
}
|
||||
|
||||
// The CredentialsManager interface for updating credentials.
|
||||
type CredentialsManager interface {
|
||||
CreatePasswordHash(password string) ([]byte, error)
|
||||
}
|
22
internal/api/middleware.go
Normal file
22
internal/api/middleware.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type idInURI struct {
|
||||
ID uint `uri:"id" binding:"required"`
|
||||
}
|
||||
|
||||
// RequireIDInURI returns a Gin middleware which requires an ID to be supplied in the URI of the request.
|
||||
func RequireIDInURI() gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
var requestModel idInURI
|
||||
|
||||
if err := ctx.BindUri(&requestModel); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Set("id", requestModel.ID)
|
||||
}
|
||||
}
|
53
internal/api/notification.go
Normal file
53
internal/api/notification.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pushbits/server/internal/authentication"
|
||||
"github.com/pushbits/server/internal/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
|
||||
DP NotificationDispatcher
|
||||
}
|
||||
|
||||
// CreateNotification is used to create a new notification for a user.
|
||||
func (h *NotificationHandler) CreateNotification(ctx *gin.Context) {
|
||||
var notification model.Notification
|
||||
|
||||
if err := ctx.Bind(¬ification); err != nil {
|
||||
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.DP.SendNotification(application, ¬ification)); !success {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, ¬ification)
|
||||
}
|
226
internal/api/user.go
Normal file
226
internal/api/user.go
Normal file
|
@ -0,0 +1,226 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/pushbits/server/internal/authentication"
|
||||
"github.com/pushbits/server/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UserHandler holds information for processing requests about users.
|
||||
type UserHandler struct {
|
||||
AH *ApplicationHandler
|
||||
CM CredentialsManager
|
||||
DB Database
|
||||
DP Dispatcher
|
||||
}
|
||||
|
||||
func (h *UserHandler) userExists(name string) bool {
|
||||
user, _ := h.DB.GetUserByName(name)
|
||||
return user != nil
|
||||
}
|
||||
|
||||
func (h *UserHandler) requireMultipleAdmins(ctx *gin.Context) error {
|
||||
if count, err := h.DB.AdminUserCount(); err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return err
|
||||
} else if count == 1 {
|
||||
err := errors.New("instance needs at least one privileged user")
|
||||
ctx.AbortWithError(http.StatusBadRequest, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *UserHandler) deleteApplications(ctx *gin.Context, u *model.User) error {
|
||||
applications, err := h.DB.GetApplications(u)
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, application := range applications {
|
||||
if err := h.AH.deleteApplication(ctx, &application, u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *UserHandler) updateChannels(ctx *gin.Context, u *model.User, matrixID string) error {
|
||||
applications, err := h.DB.GetApplications(u)
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, application := range applications {
|
||||
err := h.DP.DeregisterApplication(&application, u)
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
u.MatrixID = matrixID
|
||||
|
||||
for _, application := range applications {
|
||||
err := h.AH.registerApplication(ctx, &application, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *UserHandler) updateUser(ctx *gin.Context, u *model.User, updateUser model.UpdateUser) error {
|
||||
if updateUser.MatrixID != nil && u.MatrixID != *updateUser.MatrixID {
|
||||
if err := h.updateChannels(ctx, u, *updateUser.MatrixID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Updating user %s.\n", u.Name)
|
||||
|
||||
if updateUser.Name != nil {
|
||||
u.Name = *updateUser.Name
|
||||
}
|
||||
if updateUser.Password != nil {
|
||||
hash, err := h.CM.CreatePasswordHash(*updateUser.Password)
|
||||
if success := successOrAbort(ctx, http.StatusBadRequest, err); !success {
|
||||
return err
|
||||
}
|
||||
|
||||
u.PasswordHash = hash
|
||||
}
|
||||
if updateUser.MatrixID != nil {
|
||||
u.MatrixID = *updateUser.MatrixID
|
||||
}
|
||||
if updateUser.IsAdmin != nil {
|
||||
u.IsAdmin = *updateUser.IsAdmin
|
||||
}
|
||||
|
||||
err := h.DB.UpdateUser(u)
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateUser creates a new user.
|
||||
// This method assumes that the requesting user has privileges.
|
||||
func (h *UserHandler) CreateUser(ctx *gin.Context) {
|
||||
var createUser model.CreateUser
|
||||
|
||||
if err := ctx.Bind(&createUser); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if h.userExists(createUser.Name) {
|
||||
ctx.AbortWithError(http.StatusBadRequest, errors.New("username already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Creating user %s.\n", createUser.Name)
|
||||
|
||||
user, err := h.DB.CreateUser(createUser)
|
||||
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, user.IntoExternalUser())
|
||||
}
|
||||
|
||||
// GetUsers returns all users.
|
||||
// This method assumes that the requesting user has privileges.
|
||||
func (h *UserHandler) GetUsers(ctx *gin.Context) {
|
||||
users, err := h.DB.GetUsers()
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
|
||||
return
|
||||
}
|
||||
|
||||
var externalUsers []*model.ExternalUser
|
||||
|
||||
for _, user := range users {
|
||||
externalUsers = append(externalUsers, user.IntoExternalUser())
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &externalUsers)
|
||||
}
|
||||
|
||||
// GetUser returns the user with the specified ID.
|
||||
// This method assumes that the requesting user has privileges.
|
||||
func (h *UserHandler) GetUser(ctx *gin.Context) {
|
||||
user, err := getUser(ctx, h.DB)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, user.IntoExternalUser())
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user with a certain ID.
|
||||
//
|
||||
// This method assumes that the requesting user has privileges.
|
||||
func (h *UserHandler) DeleteUser(ctx *gin.Context) {
|
||||
user, err := getUser(ctx, h.DB)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Last privileged user must not be deleted.
|
||||
if user.IsAdmin {
|
||||
if err := h.requireMultipleAdmins(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Deleting user %s.\n", user.Name)
|
||||
|
||||
if err := h.deleteApplications(ctx, user); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, h.DB.DeleteUser(user)); !success {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
// UpdateUser updates a user with a certain ID.
|
||||
//
|
||||
// This method assumes that the requesting user has privileges. If users can later update their own user, make sure they
|
||||
// cannot give themselves privileges.
|
||||
func (h *UserHandler) UpdateUser(ctx *gin.Context) {
|
||||
user, err := getUser(ctx, h.DB)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var updateUser model.UpdateUser
|
||||
if err := ctx.BindUri(&updateUser); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
requestingUser := authentication.GetUser(ctx)
|
||||
|
||||
// Last privileged user must not be taken privileges. Assumes that the current user has privileges.
|
||||
if user.ID == requestingUser.ID && updateUser.IsAdmin != nil && !(*updateUser.IsAdmin) {
|
||||
if err := h.requireMultipleAdmins(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.updateUser(ctx, user, updateUser); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{})
|
||||
}
|
29
internal/api/util.go
Normal file
29
internal/api/util.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/pushbits/server/internal/authentication"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
func isCurrentUser(ctx *gin.Context, ID uint) bool {
|
||||
user := authentication.GetUser(ctx)
|
||||
|
||||
if user.ID != ID {
|
||||
ctx.AbortWithError(http.StatusForbidden, errors.New("only owner can delete application"))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
12
internal/assert/assert.go
Normal file
12
internal/assert/assert.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package assert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Assert panics if condition is false.
|
||||
func Assert(condition bool) {
|
||||
if !condition {
|
||||
panic(errors.New("assertion failed"))
|
||||
}
|
||||
}
|
106
internal/authentication/authentication.go
Normal file
106
internal/authentication/authentication.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/pushbits/server/internal/authentication/credentials"
|
||||
"github.com/pushbits/server/internal/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)
|
||||
}
|
||||
}
|
32
internal/authentication/context.go
Normal file
32
internal/authentication/context.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/pushbits/server/internal/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 nil
|
||||
}
|
||||
|
||||
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 nil
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
33
internal/authentication/credentials/credentials.go
Normal file
33
internal/authentication/credentials/credentials.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package credentials
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/pushbits/server/internal/configuration"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
)
|
||||
|
||||
// Manager holds information for managing credentials.
|
||||
type Manager struct {
|
||||
checkHIBP bool
|
||||
argon2Params *argon2id.Params
|
||||
}
|
||||
|
||||
// CreateManager instanciates a credential manager.
|
||||
func CreateManager(checkHIBP bool, c configuration.CryptoConfig) *Manager {
|
||||
log.Println("Setting up credential manager.")
|
||||
|
||||
argon2Params := &argon2id.Params{
|
||||
Memory: c.Argon2.Memory,
|
||||
Iterations: c.Argon2.Iterations,
|
||||
Parallelism: c.Argon2.Parallelism,
|
||||
SaltLength: c.Argon2.SaltLength,
|
||||
KeyLength: c.Argon2.KeyLength,
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
checkHIBP: checkHIBP,
|
||||
argon2Params: argon2Params,
|
||||
}
|
||||
}
|
61
internal/authentication/credentials/hibp.go
Normal file
61
internal/authentication/credentials/hibp.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package credentials
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
base = "https://api.pwnedpasswords.com"
|
||||
pwnedHashesEndpoint = "/range"
|
||||
pwnedHashesURL = base + pwnedHashesEndpoint + "/"
|
||||
)
|
||||
|
||||
// IsPasswordPwned determines whether or not the password is weak.
|
||||
func IsPasswordPwned(password string) (bool, error) {
|
||||
if len(password) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
hash := sha1.Sum([]byte(password))
|
||||
hashStr := fmt.Sprintf("%X", hash)
|
||||
lookup := hashStr[0:5]
|
||||
match := hashStr[5:]
|
||||
|
||||
log.Printf("Checking HIBP for hashes starting with '%s'.\n", lookup)
|
||||
|
||||
resp, err := http.Get(pwnedHashesURL + lookup)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Fatalf("Request failed with HTTP %s.", resp.Status)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
bodyText, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
bodyStr := string(bodyText)
|
||||
lines := strings.Split(bodyStr, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
separated := strings.Split(line, ":")
|
||||
if len(separated) != 2 {
|
||||
return false, fmt.Errorf("HIPB API returned malformed response: %s", line)
|
||||
}
|
||||
|
||||
if separated[0] == match {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
23
internal/authentication/credentials/hibp_test.go
Normal file
23
internal/authentication/credentials/hibp_test.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package credentials
|
||||
|
||||
import "testing"
|
||||
|
||||
type isPasswordPwnedTest struct {
|
||||
arg string
|
||||
exp1 bool
|
||||
exp2 error
|
||||
}
|
||||
|
||||
var isPasswordPwnedTests = []isPasswordPwnedTest{
|
||||
{"", true, nil},
|
||||
{"password", true, nil},
|
||||
{"2y6bWMETuHpNP08HCZq00QAAzE6nmwEb", false, nil},
|
||||
}
|
||||
|
||||
func TestIsPasswordPwned(t *testing.T) {
|
||||
for _, test := range isPasswordPwnedTests {
|
||||
if out1, out2 := IsPasswordPwned(test.arg); out1 != test.exp1 || out2 != test.exp2 {
|
||||
t.Errorf("Output (%t,%q) not equal to expected (%t,%q)", out1, out2, test.exp1, test.exp2)
|
||||
}
|
||||
}
|
||||
}
|
41
internal/authentication/credentials/password.go
Normal file
41
internal/authentication/credentials/password.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package credentials
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
)
|
||||
|
||||
// CreatePasswordHash returns a hashed version of the given password.
|
||||
func (m *Manager) CreatePasswordHash(password string) ([]byte, error) {
|
||||
if m.checkHIBP {
|
||||
pwned, err := IsPasswordPwned(password)
|
||||
if err != nil {
|
||||
return []byte{}, errors.New("HIBP is not available, please wait until service is available again")
|
||||
} else if pwned {
|
||||
return []byte{}, errors.New("Password is pwned, please choose another one")
|
||||
}
|
||||
}
|
||||
|
||||
hash, err := argon2id.CreateHash(password, m.argon2Params)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return []byte(hash), nil
|
||||
}
|
||||
|
||||
// ComparePassword compares a hashed password with its possible plaintext equivalent.
|
||||
func ComparePassword(hash, password []byte) bool {
|
||||
match, err := argon2id.ComparePasswordAndHash(string(password), string(hash))
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return false
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
54
internal/authentication/token.go
Normal file
54
internal/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)
|
||||
}
|
66
internal/configuration/configuration.go
Normal file
66
internal/configuration/configuration.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package configuration
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/configor"
|
||||
)
|
||||
|
||||
// Argon2Config holds the parameters used for creating hashes with Argon2.
|
||||
type Argon2Config struct {
|
||||
Memory uint32 `default:"131072"`
|
||||
Iterations uint32 `default:"4"`
|
||||
Parallelism uint8 `default:"4"`
|
||||
SaltLength uint32 `default:"16"`
|
||||
KeyLength uint32 `default:"32"`
|
||||
}
|
||||
|
||||
// CryptoConfig holds the parameters used for creating hashes.
|
||||
type CryptoConfig struct {
|
||||
Argon2 Argon2Config
|
||||
}
|
||||
|
||||
// Configuration holds values that can be configured by the user.
|
||||
type Configuration struct {
|
||||
Debug bool `default:"false"`
|
||||
HTTP struct {
|
||||
ListenAddress string `default:""`
|
||||
Port int `default:"8080"`
|
||||
}
|
||||
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"`
|
||||
}
|
||||
Security struct {
|
||||
CheckHIBP bool `default:"false"`
|
||||
}
|
||||
Crypto CryptoConfig
|
||||
}
|
||||
|
||||
func configFiles() []string {
|
||||
return []string{"config.yml"}
|
||||
}
|
||||
|
||||
// Get returns the configuration extracted from env variables or config file.
|
||||
func Get() *Configuration {
|
||||
config := &Configuration{}
|
||||
|
||||
err := configor.New(&configor.Config{
|
||||
Environment: "production",
|
||||
ENVPrefix: "PUSHBITS",
|
||||
ErrorOnUnmatchedKeys: true,
|
||||
}).Load(config, configFiles()...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
55
internal/database/application.go
Normal file
55
internal/database/application.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/pushbits/server/internal/assert"
|
||||
"github.com/pushbits/server/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CreateApplication creates an application.
|
||||
func (d *Database) CreateApplication(application *model.Application) error {
|
||||
return d.gormdb.Create(application).Error
|
||||
}
|
||||
|
||||
// DeleteApplication deletes an application.
|
||||
func (d *Database) DeleteApplication(application *model.Application) error {
|
||||
return d.gormdb.Delete(application).Error
|
||||
}
|
||||
|
||||
// UpdateApplication updates an application.
|
||||
func (d *Database) UpdateApplication(application *model.Application) error {
|
||||
return d.gormdb.Save(application).Error
|
||||
}
|
||||
|
||||
// GetApplicationByID returns the application with the given ID or nil.
|
||||
func (d *Database) GetApplicationByID(ID uint) (*model.Application, error) {
|
||||
var application model.Application
|
||||
|
||||
err := d.gormdb.First(&application, ID).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assert.Assert(application.ID == ID)
|
||||
|
||||
return &application, err
|
||||
}
|
||||
|
||||
// GetApplicationByToken returns the application with the given token or nil.
|
||||
func (d *Database) GetApplicationByToken(token string) (*model.Application, error) {
|
||||
var application model.Application
|
||||
|
||||
err := d.gormdb.Where("token = ?", token).First(&application).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assert.Assert(application.Token == token)
|
||||
|
||||
return &application, err
|
||||
}
|
100
internal/database/database.go
Normal file
100
internal/database/database.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pushbits/server/internal/authentication/credentials"
|
||||
"github.com/pushbits/server/internal/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
|
||||
credentialsManager *credentials.Manager
|
||||
}
|
||||
|
||||
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(cm *credentials.Manager, 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, credentialsManager: cm}, 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 {
|
||||
var user model.User
|
||||
|
||||
query := d.gormdb.Where("name = ?", name).First(&user)
|
||||
|
||||
if errors.Is(query.Error, gorm.ErrRecordNotFound) {
|
||||
user, err := model.NewUser(d.credentialsManager, name, password, true, matrixID)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
6
internal/database/health.go
Normal file
6
internal/database/health.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package database
|
||||
|
||||
// Health reports the status of the database connection.
|
||||
func (d *Database) Health() error {
|
||||
return d.sqldb.Ping()
|
||||
}
|
91
internal/database/user.go
Normal file
91
internal/database/user.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/pushbits/server/internal/assert"
|
||||
"github.com/pushbits/server/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CreateUser creates a user.
|
||||
func (d *Database) CreateUser(createUser model.CreateUser) (*model.User, error) {
|
||||
user, err := createUser.IntoInternalUser(d.credentialsManager)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, d.gormdb.Create(user).Error
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user.
|
||||
func (d *Database) DeleteUser(user *model.User) error {
|
||||
if err := d.gormdb.Where("user_id = ?", user.ID).Delete(model.Application{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return d.gormdb.Delete(user).Error
|
||||
}
|
||||
|
||||
// UpdateUser updates a user.
|
||||
func (d *Database) UpdateUser(user *model.User) error {
|
||||
return d.gormdb.Save(user).Error
|
||||
}
|
||||
|
||||
// GetUserByID returns the user with the given ID or nil.
|
||||
func (d *Database) GetUserByID(ID uint) (*model.User, error) {
|
||||
var user model.User
|
||||
|
||||
err := d.gormdb.First(&user, ID).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assert.Assert(user.ID == ID)
|
||||
|
||||
return &user, err
|
||||
}
|
||||
|
||||
// GetUserByName returns the user with the given name or nil.
|
||||
func (d *Database) GetUserByName(name string) (*model.User, error) {
|
||||
var user model.User
|
||||
|
||||
err := d.gormdb.Where("name = ?", name).First(&user).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assert.Assert(user.Name == name)
|
||||
|
||||
return &user, err
|
||||
}
|
||||
|
||||
// GetApplications returns the applications associated with a given user.
|
||||
func (d *Database) GetApplications(user *model.User) ([]model.Application, error) {
|
||||
var applications []model.Application
|
||||
|
||||
err := d.gormdb.Model(user).Association("Applications").Find(&applications)
|
||||
|
||||
return applications, err
|
||||
}
|
||||
|
||||
// GetUsers returns all users.
|
||||
func (d *Database) GetUsers() ([]model.User, error) {
|
||||
var users []model.User
|
||||
|
||||
err := d.gormdb.Find(&users).Error
|
||||
|
||||
return users, err
|
||||
}
|
||||
|
||||
// AdminUserCount returns the number of admins or an error.
|
||||
func (d *Database) AdminUserCount() (int64, error) {
|
||||
var users []model.User
|
||||
|
||||
query := d.gormdb.Where("is_admin = ?", true).Find(&users)
|
||||
|
||||
return query.RowsAffected, query.Error
|
||||
}
|
62
internal/dispatcher/application.go
Normal file
62
internal/dispatcher/application.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package dispatcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/pushbits/server/internal/model"
|
||||
|
||||
"github.com/matrix-org/gomatrix"
|
||||
)
|
||||
|
||||
// RegisterApplication creates a channel for an application.
|
||||
func (d *Dispatcher) RegisterApplication(id uint, name, token, user string) (string, error) {
|
||||
log.Printf("Registering application %s, notifications will be relayed to user %s.\n", name, user)
|
||||
|
||||
topic := fmt.Sprintf("Application %d, Token %s", id, token)
|
||||
|
||||
response, err := d.client.CreateRoom(&gomatrix.ReqCreateRoom{
|
||||
Invite: []string{user},
|
||||
IsDirect: true,
|
||||
Name: name,
|
||||
Preset: "private_chat",
|
||||
Topic: topic,
|
||||
Visibility: "private",
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// DeregisterApplication deletes a channel for an application.
|
||||
func (d *Dispatcher) DeregisterApplication(a *model.Application, u *model.User) error {
|
||||
log.Printf("Deregistering application %s (ID %d) with Matrix ID %s.\n", a.Name, a.ID, a.MatrixID)
|
||||
|
||||
kickUser := &gomatrix.ReqKickUser{
|
||||
Reason: "This application was deleted",
|
||||
UserID: u.MatrixID,
|
||||
}
|
||||
|
||||
if _, err := d.client.KickUser(a.MatrixID, kickUser); err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := d.client.LeaveRoom(a.MatrixID); err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := d.client.ForgetRoom(a.MatrixID); err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
52
internal/dispatcher/dispatcher.go
Normal file
52
internal/dispatcher/dispatcher.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package dispatcher
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/matrix-org/gomatrix"
|
||||
)
|
||||
|
||||
var (
|
||||
loginType = "m.login.password"
|
||||
)
|
||||
|
||||
// The Database interface for encapsulating database access.
|
||||
type Database interface {
|
||||
}
|
||||
|
||||
// Dispatcher holds information for sending notifications to clients.
|
||||
type Dispatcher struct {
|
||||
db Database
|
||||
client *gomatrix.Client
|
||||
}
|
||||
|
||||
// Create instanciates a dispatcher connection.
|
||||
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
internal/dispatcher/notification.go
Normal file
19
internal/dispatcher/notification.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package dispatcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/pushbits/server/internal/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
|
||||
}
|
20
internal/model/application.go
Normal file
20
internal/model/application.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
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" json:"name"`
|
||||
MatrixID string `gorm:"type:string" json:"-"`
|
||||
}
|
||||
|
||||
// CreateApplication is used to process queries for creating applications.
|
||||
type CreateApplication struct {
|
||||
Name string `form:"name" query:"name" json:"name" binding:"required"`
|
||||
}
|
||||
|
||||
// UpdateApplication is used to process queries for updating applications.
|
||||
type UpdateApplication struct {
|
||||
Name *string `json:"name"`
|
||||
}
|
16
internal/model/notification.go
Normal file
16
internal/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"`
|
||||
}
|
86
internal/model/user.go
Normal file
86
internal/model/user.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/pushbits/server/internal/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"`
|
||||
}
|
||||
|
||||
// CreateUser is used to process queries for creating users.
|
||||
type CreateUser struct {
|
||||
ExternalUser
|
||||
UserCredentials
|
||||
}
|
||||
|
||||
// NewUser creates a new user.
|
||||
func NewUser(cm *credentials.Manager, name, password string, isAdmin bool, matrixID string) (*User, error) {
|
||||
log.Printf("Creating user %s.\n", name)
|
||||
|
||||
passwordHash, err := cm.CreatePasswordHash(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &User{
|
||||
Name: name,
|
||||
PasswordHash: passwordHash,
|
||||
IsAdmin: isAdmin,
|
||||
MatrixID: matrixID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IntoInternalUser converts a CreateUser into a User.
|
||||
func (u *CreateUser) IntoInternalUser(cm *credentials.Manager) (*User, error) {
|
||||
passwordHash, err := cm.CreatePasswordHash(u.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &User{
|
||||
Name: u.Name,
|
||||
PasswordHash: passwordHash,
|
||||
IsAdmin: u.IsAdmin,
|
||||
MatrixID: u.MatrixID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateUser is used to process queries for updating users.
|
||||
type UpdateUser struct {
|
||||
Name *string `json:"name"`
|
||||
Password *string `json:"password"`
|
||||
IsAdmin *bool `json:"is_admin"`
|
||||
MatrixID *string `json:"matrix_id"`
|
||||
}
|
62
internal/router/router.go
Normal file
62
internal/router/router.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/pushbits/server/internal/api"
|
||||
"github.com/pushbits/server/internal/authentication"
|
||||
"github.com/pushbits/server/internal/authentication/credentials"
|
||||
"github.com/pushbits/server/internal/database"
|
||||
"github.com/pushbits/server/internal/dispatcher"
|
||||
|
||||
"github.com/gin-contrib/location"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Create a Gin engine and setup all routes.
|
||||
func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *dispatcher.Dispatcher) *gin.Engine {
|
||||
log.Println("Setting up HTTP routes.")
|
||||
|
||||
if !debug {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
auth := authentication.Authenticator{DB: db}
|
||||
|
||||
applicationHandler := api.ApplicationHandler{DB: db, DP: dp}
|
||||
healthHandler := api.HealthHandler{DB: db}
|
||||
notificationHandler := api.NotificationHandler{DB: db, DP: dp}
|
||||
userHandler := api.UserHandler{AH: &applicationHandler, CM: cm, DB: db, DP: dp}
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
r.Use(location.Default())
|
||||
|
||||
applicationGroup := r.Group("/application")
|
||||
applicationGroup.Use(auth.RequireUser())
|
||||
{
|
||||
applicationGroup.POST("", applicationHandler.CreateApplication)
|
||||
applicationGroup.GET("", applicationHandler.GetApplications)
|
||||
|
||||
applicationGroup.GET("/:id", api.RequireIDInURI(), applicationHandler.GetApplication)
|
||||
applicationGroup.DELETE("/:id", api.RequireIDInURI(), applicationHandler.DeleteApplication)
|
||||
applicationGroup.PUT("/:id", api.RequireIDInURI(), applicationHandler.UpdateApplication)
|
||||
}
|
||||
|
||||
r.GET("/health", healthHandler.Health)
|
||||
|
||||
r.POST("/message", auth.RequireApplicationToken(), notificationHandler.CreateNotification)
|
||||
|
||||
userGroup := r.Group("/user")
|
||||
userGroup.Use(auth.RequireAdmin())
|
||||
{
|
||||
userGroup.POST("", userHandler.CreateUser)
|
||||
userGroup.GET("", userHandler.GetUsers)
|
||||
|
||||
userGroup.GET("/:id", api.RequireIDInURI(), userHandler.GetUser)
|
||||
userGroup.DELETE("/:id", api.RequireIDInURI(), userHandler.DeleteUser)
|
||||
userGroup.PUT("/:id", api.RequireIDInURI(), userHandler.UpdateUser)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
12
internal/runner/runner.go
Normal file
12
internal/runner/runner.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Run starts the Gin engine.
|
||||
func Run(engine *gin.Engine, address string, port int) {
|
||||
engine.Run(fmt.Sprintf("%s:%d", address, port))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue