470 lines
12 KiB
Go
470 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gotify/server/v2/auth"
|
|
"github.com/gotify/server/v2/model"
|
|
"github.com/h2non/filetype"
|
|
)
|
|
|
|
// The ApplicationDatabase interface for encapsulating database access.
|
|
type ApplicationDatabase interface {
|
|
CreateApplication(application *model.Application) error
|
|
GetApplicationByToken(token string) (*model.Application, error)
|
|
GetApplicationByID(id uint) (*model.Application, error)
|
|
GetApplicationsByUser(userID uint) ([]*model.Application, error)
|
|
DeleteApplicationByID(id uint) error
|
|
UpdateApplication(application *model.Application) error
|
|
}
|
|
|
|
// The ApplicationAPI provides handlers for managing applications.
|
|
type ApplicationAPI struct {
|
|
DB ApplicationDatabase
|
|
ImageDir string
|
|
}
|
|
|
|
// Application Params Model
|
|
//
|
|
// Params allowed to create or update Applications.
|
|
//
|
|
// swagger:model ApplicationParams
|
|
type ApplicationParams struct {
|
|
// The application name. This is how the application should be displayed to the user.
|
|
//
|
|
// required: true
|
|
// example: Backup Server
|
|
Name string `form:"name" query:"name" json:"name" binding:"required"`
|
|
// The description of the application.
|
|
//
|
|
// example: Backup server for the interwebs
|
|
Description string `form:"description" query:"description" json:"description"`
|
|
// The default priority of messages sent by this application. Defaults to 0.
|
|
//
|
|
// example: 5
|
|
DefaultPriority int `form:"defaultPriority" query:"defaultPriority" json:"defaultPriority"`
|
|
}
|
|
|
|
// CreateApplication creates an application and returns the access token.
|
|
// swagger:operation POST /application application createApp
|
|
//
|
|
// Create an application.
|
|
//
|
|
// ---
|
|
// consumes: [application/json]
|
|
// produces: [application/json]
|
|
// security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
|
|
// parameters:
|
|
// - name: body
|
|
// in: body
|
|
// description: the application to add
|
|
// required: true
|
|
// schema:
|
|
// $ref: "#/definitions/ApplicationParams"
|
|
// responses:
|
|
// 200:
|
|
// description: Ok
|
|
// schema:
|
|
// $ref: "#/definitions/Application"
|
|
// 400:
|
|
// description: Bad Request
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 401:
|
|
// description: Unauthorized
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 403:
|
|
// description: Forbidden
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
func (a *ApplicationAPI) CreateApplication(ctx *gin.Context) {
|
|
applicationParams := ApplicationParams{}
|
|
if err := ctx.Bind(&applicationParams); err == nil {
|
|
app := model.Application{
|
|
Name: applicationParams.Name,
|
|
Description: applicationParams.Description,
|
|
DefaultPriority: applicationParams.DefaultPriority,
|
|
Token: auth.GenerateNotExistingToken(generateApplicationToken, a.applicationExists),
|
|
UserID: auth.GetUserID(ctx),
|
|
Internal: false,
|
|
}
|
|
|
|
if success := successOrAbort(ctx, 500, a.DB.CreateApplication(&app)); !success {
|
|
return
|
|
}
|
|
ctx.JSON(200, withResolvedImage(&app))
|
|
}
|
|
}
|
|
|
|
// GetApplications returns all applications a user has.
|
|
// swagger:operation GET /application application getApps
|
|
//
|
|
// Return all applications.
|
|
//
|
|
// ---
|
|
// consumes: [application/json]
|
|
// produces: [application/json]
|
|
// security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
|
|
// responses:
|
|
// 200:
|
|
// description: Ok
|
|
// schema:
|
|
// type: array
|
|
// items:
|
|
// $ref: "#/definitions/Application"
|
|
// 401:
|
|
// description: Unauthorized
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 403:
|
|
// description: Forbidden
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
func (a *ApplicationAPI) GetApplications(ctx *gin.Context) {
|
|
userID := auth.GetUserID(ctx)
|
|
apps, err := a.DB.GetApplicationsByUser(userID)
|
|
if success := successOrAbort(ctx, 500, err); !success {
|
|
return
|
|
}
|
|
for _, app := range apps {
|
|
withResolvedImage(app)
|
|
}
|
|
ctx.JSON(200, apps)
|
|
}
|
|
|
|
// DeleteApplication deletes an application by its id.
|
|
// swagger:operation DELETE /application/{id} application deleteApp
|
|
//
|
|
// Delete an application.
|
|
//
|
|
// ---
|
|
// consumes: [application/json]
|
|
// produces: [application/json]
|
|
// parameters:
|
|
// - name: id
|
|
// in: path
|
|
// description: the application id
|
|
// required: true
|
|
// type: integer
|
|
// format: int64
|
|
// security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
|
|
// responses:
|
|
// 200:
|
|
// description: Ok
|
|
// 400:
|
|
// description: Bad Request
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 401:
|
|
// description: Unauthorized
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 403:
|
|
// description: Forbidden
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 404:
|
|
// description: Not Found
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
func (a *ApplicationAPI) DeleteApplication(ctx *gin.Context) {
|
|
withID(ctx, "id", func(id uint) {
|
|
app, err := a.DB.GetApplicationByID(id)
|
|
if success := successOrAbort(ctx, 500, err); !success {
|
|
return
|
|
}
|
|
if app != nil && app.UserID == auth.GetUserID(ctx) {
|
|
if app.Internal {
|
|
ctx.AbortWithError(400, errors.New("cannot delete internal application"))
|
|
return
|
|
}
|
|
if success := successOrAbort(ctx, 500, a.DB.DeleteApplicationByID(id)); !success {
|
|
return
|
|
}
|
|
if app.Image != "" {
|
|
os.Remove(a.ImageDir + app.Image)
|
|
}
|
|
} else {
|
|
ctx.AbortWithError(404, fmt.Errorf("app with id %d doesn't exists", id))
|
|
}
|
|
})
|
|
}
|
|
|
|
// UpdateApplication updates an application info by its id.
|
|
// swagger:operation PUT /application/{id} application updateApplication
|
|
//
|
|
// Update an application.
|
|
//
|
|
// ---
|
|
// consumes: [application/json]
|
|
// produces: [application/json]
|
|
// security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
|
|
// parameters:
|
|
// - name: body
|
|
// in: body
|
|
// description: the application to update
|
|
// required: true
|
|
// schema:
|
|
// $ref: "#/definitions/ApplicationParams"
|
|
// - name: id
|
|
// in: path
|
|
// description: the application id
|
|
// required: true
|
|
// type: integer
|
|
// format: int64
|
|
// responses:
|
|
// 200:
|
|
// description: Ok
|
|
// schema:
|
|
// $ref: "#/definitions/Application"
|
|
// 400:
|
|
// description: Bad Request
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 401:
|
|
// description: Unauthorized
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 403:
|
|
// description: Forbidden
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 404:
|
|
// description: Not Found
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
func (a *ApplicationAPI) UpdateApplication(ctx *gin.Context) {
|
|
withID(ctx, "id", func(id uint) {
|
|
app, err := a.DB.GetApplicationByID(id)
|
|
if success := successOrAbort(ctx, 500, err); !success {
|
|
return
|
|
}
|
|
if app != nil && app.UserID == auth.GetUserID(ctx) {
|
|
applicationParams := ApplicationParams{}
|
|
if err := ctx.Bind(&applicationParams); err == nil {
|
|
app.Description = applicationParams.Description
|
|
app.Name = applicationParams.Name
|
|
app.DefaultPriority = applicationParams.DefaultPriority
|
|
|
|
if success := successOrAbort(ctx, 500, a.DB.UpdateApplication(app)); !success {
|
|
return
|
|
}
|
|
ctx.JSON(200, withResolvedImage(app))
|
|
}
|
|
} else {
|
|
ctx.AbortWithError(404, fmt.Errorf("app with id %d doesn't exists", id))
|
|
}
|
|
})
|
|
}
|
|
|
|
// UploadApplicationImage uploads an image for an application.
|
|
// swagger:operation POST /application/{id}/image application uploadAppImage
|
|
//
|
|
// Upload an image for an application.
|
|
//
|
|
// ---
|
|
// consumes:
|
|
// - multipart/form-data
|
|
// produces: [application/json]
|
|
// security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
|
|
// parameters:
|
|
// - name: file
|
|
// in: formData
|
|
// description: the application image
|
|
// required: true
|
|
// type: file
|
|
// - name: id
|
|
// in: path
|
|
// description: the application id
|
|
// required: true
|
|
// type: integer
|
|
// format: int64
|
|
// responses:
|
|
// 200:
|
|
// description: Ok
|
|
// schema:
|
|
// $ref: "#/definitions/Application"
|
|
// 400:
|
|
// description: Bad Request
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 401:
|
|
// description: Unauthorized
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 403:
|
|
// description: Forbidden
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 404:
|
|
// description: Not Found
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 500:
|
|
// description: Server Error
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
func (a *ApplicationAPI) UploadApplicationImage(ctx *gin.Context) {
|
|
withID(ctx, "id", func(id uint) {
|
|
app, err := a.DB.GetApplicationByID(id)
|
|
if success := successOrAbort(ctx, 500, err); !success {
|
|
return
|
|
}
|
|
if app != nil && app.UserID == auth.GetUserID(ctx) {
|
|
file, err := ctx.FormFile("file")
|
|
if err == http.ErrMissingFile {
|
|
ctx.AbortWithError(400, errors.New("file with key 'file' must be present"))
|
|
return
|
|
} else if err != nil {
|
|
ctx.AbortWithError(500, err)
|
|
return
|
|
}
|
|
head := make([]byte, 261)
|
|
open, _ := file.Open()
|
|
open.Read(head)
|
|
if !filetype.IsImage(head) {
|
|
ctx.AbortWithError(400, errors.New("file must be an image"))
|
|
return
|
|
}
|
|
|
|
ext := filepath.Ext(file.Filename)
|
|
if !ValidApplicationImageExt(ext) {
|
|
ctx.AbortWithError(400, errors.New("invalid file extension"))
|
|
return
|
|
}
|
|
|
|
name := generateNonExistingImageName(a.ImageDir, func() string {
|
|
return generateImageName() + ext
|
|
})
|
|
|
|
err = ctx.SaveUploadedFile(file, a.ImageDir+name)
|
|
if err != nil {
|
|
ctx.AbortWithError(500, err)
|
|
return
|
|
}
|
|
|
|
if app.Image != "" {
|
|
os.Remove(a.ImageDir + app.Image)
|
|
}
|
|
|
|
app.Image = name
|
|
if success := successOrAbort(ctx, 500, a.DB.UpdateApplication(app)); !success {
|
|
return
|
|
}
|
|
ctx.JSON(200, withResolvedImage(app))
|
|
} else {
|
|
ctx.AbortWithError(404, fmt.Errorf("app with id %d doesn't exists", id))
|
|
}
|
|
})
|
|
}
|
|
|
|
// RemoveApplicationImage deletes an image of an application.
|
|
// swagger:operation DELETE /application/{id}/image application removeAppImage
|
|
//
|
|
// Deletes an image of an application.
|
|
//
|
|
// ---
|
|
// consumes: [application/json]
|
|
// produces: [application/json]
|
|
// parameters:
|
|
// - name: id
|
|
// in: path
|
|
// description: the application id
|
|
// required: true
|
|
// type: integer
|
|
// format: int64
|
|
// security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
|
|
// responses:
|
|
// 200:
|
|
// description: Ok
|
|
// 400:
|
|
// description: Bad Request
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 401:
|
|
// description: Unauthorized
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 403:
|
|
// description: Forbidden
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 404:
|
|
// description: Not Found
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
// 500:
|
|
// description: Server Error
|
|
// schema:
|
|
// $ref: "#/definitions/Error"
|
|
func (a *ApplicationAPI) RemoveApplicationImage(ctx *gin.Context) {
|
|
withID(ctx, "id", func(id uint) {
|
|
app, err := a.DB.GetApplicationByID(id)
|
|
if success := successOrAbort(ctx, 500, err); !success {
|
|
return
|
|
}
|
|
if app != nil && app.UserID == auth.GetUserID(ctx) {
|
|
if app.Image == "" {
|
|
ctx.AbortWithError(400, fmt.Errorf("app with id %d does not have a customized image", id))
|
|
return
|
|
}
|
|
|
|
image := app.Image
|
|
app.Image = ""
|
|
if success := successOrAbort(ctx, 500, a.DB.UpdateApplication(app)); !success {
|
|
return
|
|
}
|
|
os.Remove(a.ImageDir + image)
|
|
ctx.JSON(200, withResolvedImage(app))
|
|
} else {
|
|
ctx.AbortWithError(404, fmt.Errorf("app with id %d doesn't exists", id))
|
|
}
|
|
})
|
|
}
|
|
|
|
func withResolvedImage(app *model.Application) *model.Application {
|
|
if app.Image == "" {
|
|
app.Image = "static/defaultapp.png"
|
|
} else {
|
|
app.Image = "image/" + app.Image
|
|
}
|
|
return app
|
|
}
|
|
|
|
func (a *ApplicationAPI) applicationExists(token string) bool {
|
|
app, _ := a.DB.GetApplicationByToken(token)
|
|
return app != nil
|
|
}
|
|
|
|
func exist(path string) bool {
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func generateNonExistingImageName(imgDir string, gen func() string) string {
|
|
for {
|
|
name := gen()
|
|
if !exist(imgDir + name) {
|
|
return name
|
|
}
|
|
}
|
|
}
|
|
|
|
func ValidApplicationImageExt(ext string) bool {
|
|
switch strings.ToLower(ext) {
|
|
case ".gif", ".png", ".jpg", ".jpeg":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|