Add basic swagger documentation

accessible by running with -swagger. Accessible at /swagger/index.html.
Currently doesn't have authentication setup, so no requests will work.
This commit is contained in:
Harvey Tindall 2020-09-24 17:51:13 +01:00
parent 544f5674e8
commit b6537cef65
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
12 changed files with 3254 additions and 144 deletions

View File

@ -36,6 +36,9 @@ ts-debug:
-npx tsc -p ts/ --sourceMap
cp -r ts data/static/
swagger:
swag init -g main.go
version:
python3 version.py auto version.go
@ -52,6 +55,7 @@ compress:
copy:
$(info Copying data)
cp -r data build/
cp docs/swagger.json build/data/static/
install:
cp -r build $(DESTDIR)/jfa-go

450
api.go
View File

@ -13,6 +13,38 @@ import (
"gopkg.in/ini.v1"
)
func respond(code int, message string, gc *gin.Context) {
resp := stringResponse{}
if code == 200 || code == 204 {
resp.Response = message
} else {
resp.Error = message
}
gc.JSON(code, resp)
gc.Abort()
}
type stringResponse struct {
Response string `json:"response" example:"message"`
Error string `json:"error" example:"errorDescription"`
}
type boolResponse struct {
Success bool `json:"success" example:"false"`
Error bool `json:"error" example:"true"`
}
func respondBool(code int, val bool, gc *gin.Context) {
resp := boolResponse{}
if !val {
resp.Error = true
} else {
resp.Success = true
}
gc.JSON(code, resp)
gc.Abort()
}
func (app *appContext) loadStrftime() {
app.datePattern = app.config.Section("email").Key("date_format").String()
app.timePattern = `%H:%M`
@ -174,15 +206,20 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
// Routes from now on!
type newUserReq struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Code string `json:"code"`
type newUserDTO struct {
Username string `json:"username" example:"jeff" binding:"required"` // User's username
Password string `json:"password" example:"guest" binding:"required"` // User's password
Email string `json:"email" example:"jeff@jellyf.in"` // User's email address
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
}
// @Summary Creates a new Jellyfin user without an invite.
// @Produce json
// @Param newUserDTO body newUserDTO true "New user request object"
// @Success 200
// @Router /users [post]
func (app *appContext) NewUserAdmin(gc *gin.Context) {
var req newUserReq
var req newUserDTO
gc.BindJSON(&req)
existingUser, _, _ := app.jf.userByName(req.Username, false)
if existingUser != nil {
@ -234,14 +271,19 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
app.jf.cacheExpiry = time.Now()
}
// @Summary Creates a new Jellyfin user via invite code
// @Produce json
// @Param newUserDTO body newUserDTO true "New user request object"
// @Success 200 {object} PasswordValidation
// @Failure 400 {object} PasswordValidation
// @Router /newUser [post]
func (app *appContext) NewUser(gc *gin.Context) {
var req newUserReq
var req newUserDTO
gc.BindJSON(&req)
app.debug.Printf("%s: New user attempt", req.Code)
if !app.checkInvite(req.Code, false, "") {
app.info.Printf("%s New user failed: invalid code", req.Code)
gc.JSON(401, map[string]bool{"success": false})
gc.Abort()
respondBool(401, false, gc)
return
}
validation := app.validator.validate(req.Password)
@ -335,17 +377,30 @@ func (app *appContext) NewUser(gc *gin.Context) {
}
}
}
gc.JSON(200, validation)
code := 200
for _, val := range validation {
if !val {
code = 400
}
}
gc.JSON(code, validation)
}
type deleteUserReq struct {
Users []string `json:"users"`
Notify bool `json:"notify"`
Reason string `json:"reason"`
type deleteUserDTO struct {
Users []string `json:"users" binding:"required"` // List of usernames to delete
Notify bool `json:"notify"` // Whether to notify users of deletion
Reason string `json:"reason"` // Account deletion reason (for notification)
}
// @Summary Delete a list of users, optionally notifying them why.
// @Produce json
// @Param deleteUserDTO body deleteUserDTO true "User deletion request object"
// @Success 200 {object} boolResponse
// @Failure 400 {object} stringResponse
// @Failure 500 {object} errorListDTO "List of errors"
// @Router /users [delete]
func (app *appContext) DeleteUser(gc *gin.Context) {
var req deleteUserReq
var req deleteUserDTO
gc.BindJSON(&req)
errors := map[string]string{}
for _, userID := range req.Users {
@ -373,29 +428,34 @@ func (app *appContext) DeleteUser(gc *gin.Context) {
}
app.jf.cacheExpiry = time.Now()
if len(errors) == len(req.Users) {
respond(500, "Failed", gc)
respondBool(500, false, gc)
app.err.Printf("Account deletion failed: %s", errors[req.Users[0]])
return
} else if len(errors) != 0 {
gc.JSON(500, errors)
return
}
gc.JSON(200, map[string]bool{"success": true})
respondBool(200, true, gc)
}
type generateInviteReq struct {
Days int `json:"days"`
Hours int `json:"hours"`
Minutes int `json:"minutes"`
Email string `json:"email"`
MultipleUses bool `json:"multiple-uses"`
NoLimit bool `json:"no-limit"`
RemainingUses int `json:"remaining-uses"`
Profile string `json:"profile"`
type generateInviteDTO struct {
Days int `json:"days" example:"1"` // Number of days
Hours int `json:"hours" example:"2"` // Number of hours
Minutes int `json:"minutes" example:"3"` // Number of minutes
Email string `json:"email" example:"jeff@jellyf.in"` // Send invite to this address
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
Profile string `json:"profile" example:"DefaultProfile"` // Name of profile to apply on this invite
}
// @Summary Create a new invite.
// @Produce json
// @Param generateInviteDTO body generateInviteDTO true "New invite request object"
// @Success 200 {object} boolResponse
// @Router /invites [post]
func (app *appContext) GenerateInvite(gc *gin.Context) {
var req generateInviteReq
var req generateInviteDTO
app.debug.Println("Generating new invite")
app.storage.loadInvites()
gc.BindJSON(&req)
@ -446,16 +506,22 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
}
app.storage.invites[invite_code] = invite
app.storage.storeInvites()
gc.JSON(200, map[string]bool{"success": true})
respondBool(200, true, gc)
}
type profileReq struct {
Invite string `json:"invite"`
Profile string `json:"profile"`
type inviteProfileDTO struct {
Invite string `json:"invite" example:"slakdaslkdl2342"` // Invite to apply to
Profile string `json:"profile" example:"DefaultProfile"` // Profile to use
}
// @Summary Set profile for an invite
// @Produce json
// @Param inviteProfileDTO body inviteProfileDTO true "Invite profile object"
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /invites/profile [post]
func (app *appContext) SetProfile(gc *gin.Context) {
var req profileReq
var req inviteProfileDTO
gc.BindJSON(&req)
app.debug.Printf("%s: Setting profile to \"%s\"", req.Invite, req.Profile)
// "" means "Don't apply profile"
@ -468,56 +534,87 @@ func (app *appContext) SetProfile(gc *gin.Context) {
inv.Profile = req.Profile
app.storage.invites[req.Invite] = inv
app.storage.storeInvites()
gc.JSON(200, map[string]bool{"success": true})
respondBool(200, true, gc)
}
type profileDTO struct {
Admin bool `json:"admin" example:"false"` // Whether profile has admin rights or not
LibraryAccess string `json:"libraries" example:"all"` // Number of libraries profile has access to
FromUser string `json:"fromUser" example:"jeff"` // The user the profile is based on
}
type getProfilesDTO struct {
Profiles map[string]profileDTO `json:"profiles"`
DefaultProfile string `json:"default_profile"`
}
// @Summary Get a list of profiles
// @Produce json
// @Success 200 {object} getProfilesDTO
// @Router /profiles [get]
func (app *appContext) GetProfiles(gc *gin.Context) {
app.storage.loadProfiles()
app.debug.Println("Profiles requested")
out := map[string]interface{}{
"default_profile": app.storage.defaultProfile,
out := getProfilesDTO{
DefaultProfile: app.storage.defaultProfile,
Profiles: map[string]profileDTO{},
}
for name, p := range app.storage.profiles {
out[name] = map[string]interface{}{
"admin": p.Admin,
"libraries": p.LibraryAccess,
"fromUser": p.FromUser,
out.Profiles[name] = profileDTO{
Admin: p.Admin,
LibraryAccess: p.LibraryAccess,
FromUser: p.FromUser,
}
}
fmt.Println(out)
gc.JSON(200, out)
}
type profileChangeDTO struct {
Name string `json:"name" example:"DefaultProfile" binding:"required"` // Name of the profile
}
// @Summary Set the default profile to use.
// @Produce json
// @Param profileChangeDTO body profileChangeDTO true "Default profile object"
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /profiles/default [post]
func (app *appContext) SetDefaultProfile(gc *gin.Context) {
req := map[string]string{}
req := profileChangeDTO{}
gc.BindJSON(&req)
app.info.Printf("Setting default profile to \"%s\"", req["name"])
if _, ok := app.storage.profiles[req["name"]]; !ok {
app.err.Printf("Profile not found: \"%s\"", req["name"])
app.info.Printf("Setting default profile to \"%s\"", req.Name)
if _, ok := app.storage.profiles[req.Name]; !ok {
app.err.Printf("Profile not found: \"%s\"", req.Name)
respond(500, "Profile not found", gc)
return
}
for name, profile := range app.storage.profiles {
if name == req["name"] {
if name == req.Name {
profile.Admin = true
app.storage.profiles[name] = profile
} else {
profile.Admin = false
}
}
app.storage.defaultProfile = req["name"]
gc.JSON(200, map[string]bool{"success": true})
app.storage.defaultProfile = req.Name
respondBool(200, true, gc)
}
type newProfileReq struct {
Name string `json:"name"`
ID string `json:"id"`
Homescreen bool `json:"homescreen"`
type newProfileDTO struct {
Name string `json:"name" example:"DefaultProfile" binding:"required"` // Name of the profile
ID string `json:"id" example:"kasdjlaskjd342342" binding:"required"` // ID of user to source settings from
Homescreen bool `json:"homescreen" example:"true"` // Whether to store homescreen layout or not
}
// @Summary Create a profile based on a Jellyfin user's settings.
// @Produce json
// @Param newProfileDTO body newProfileDTO true "New profile object"
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /profiles [post]
func (app *appContext) CreateProfile(gc *gin.Context) {
fmt.Println("Profile creation requested")
var req newProfileReq
var req newProfileDTO
gc.BindJSON(&req)
user, status, err := app.jf.userById(req.ID, false)
if !(status == 200 || status == 204) || err != nil {
@ -545,48 +642,75 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
app.storage.profiles[req.Name] = profile
app.storage.storeProfiles()
app.storage.loadProfiles()
gc.JSON(200, map[string]bool{"success": true})
respondBool(200, true, gc)
}
// @Summary Delete an existing profile
// @Produce json
// @Param profileChangeDTO body profileChangeDTO true "Delete profile object"
// @Success 200 {object} boolResponse
// @Router /profiles [delete]
func (app *appContext) DeleteProfile(gc *gin.Context) {
req := map[string]string{}
req := profileChangeDTO{}
gc.BindJSON(&req)
name := req["name"]
name := req.Name
if _, ok := app.storage.profiles[name]; ok {
delete(app.storage.profiles, name)
}
app.storage.storeProfiles()
gc.JSON(200, map[string]bool{"success": true})
respondBool(200, true, gc)
}
type inviteDTO struct {
Code string `json:"code" example:"sajdlj23423j23"` // Invite code
Days int `json:"days" example:"1"` // Number of days till expiry
Hours int `json:"hours" example:"2"` // Number of hours till expiry
Minutes int `json:"minutes" example:"3"` // Number of minutes till expiry
Created string `json:"created" example:"01/01/20 12:00"` // Date of creation
Profile string `json:"profile" example:"DefaultProfile"` // Profile used on this invite
UsedBy [][]string `json:"used-by,omitempty"` // Users who have used this invite
NoLimit bool `json:"no-limit,omitempty"` // If true, invite can be used any number of times
RemainingUses int `json:"remaining-uses,omitempty"` // Remaining number of uses (if applicable)
Email string `json:"email,omitempty"` // Email the invite was sent to (if applicable)
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
}
type getInvitesDTO struct {
Profiles []string `json:"profiles"` // List of profiles (name only)
Invites []inviteDTO `json:"invites"` // List of invites
}
// @Summary Get invites.
// @Produce json
// @Success 200 {object} getInvitesDTO
// @Router /invites [get]
func (app *appContext) GetInvites(gc *gin.Context) {
app.debug.Println("Invites requested")
current_time := time.Now()
app.storage.loadInvites()
app.checkInvites()
var invites []map[string]interface{}
var invites []inviteDTO
for code, inv := range app.storage.invites {
_, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, current_time)
invite := map[string]interface{}{
"code": code,
"days": days,
"hours": hours,
"minutes": minutes,
"created": app.formatDatetime(inv.Created),
"profile": inv.Profile,
invite := inviteDTO{
Code: code,
Days: days,
Hours: hours,
Minutes: minutes,
Created: app.formatDatetime(inv.Created),
Profile: inv.Profile,
NoLimit: inv.NoLimit,
}
if len(inv.UsedBy) != 0 {
invite["used-by"] = inv.UsedBy
invite.UsedBy = inv.UsedBy
}
if inv.NoLimit {
invite["no-limit"] = true
}
invite["remaining-uses"] = 1
invite.RemainingUses = 1
if inv.RemainingUses != 0 {
invite["remaining-uses"] = inv.RemainingUses
invite.RemainingUses = inv.RemainingUses
}
if inv.Email != "" {
invite["email"] = inv.Email
invite.Email = inv.Email
}
if len(inv.Notify) != 0 {
var address string
@ -599,10 +723,11 @@ func (app *appContext) GetInvites(gc *gin.Context) {
address = app.config.Section("ui").Key("email").String()
}
if _, ok := inv.Notify[address]; ok {
for _, notifyType := range []string{"notify-expiry", "notify-creation"} {
if _, ok = inv.Notify[address][notifyType]; ok {
invite[notifyType] = inv.Notify[address][notifyType]
}
if _, ok = inv.Notify[address]["notify-expiry"]; ok {
invite.NotifyExpiry = inv.Notify[address]["notify-expiry"]
}
if _, ok = inv.Notify[address]["notify-creation"]; ok {
invite.NotifyCreation = inv.Notify[address]["notify-creation"]
}
}
}
@ -621,13 +746,28 @@ func (app *appContext) GetInvites(gc *gin.Context) {
}
}
}
resp := map[string]interface{}{
"profiles": profiles,
"invites": invites,
resp := getInvitesDTO{
Profiles: profiles,
Invites: invites,
}
gc.JSON(200, resp)
}
// fake DTO, if i actually used this the code would be a lot longer
type setNotifyValues map[string]struct {
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
}
type setNotifyDTO map[string]setNotifyValues
// @Summary Set notification preferences for an invite.
// @Produce json
// @Param setNotifyDTO body setNotifyDTO true "Map of invite codes to notification settings objects"
// @Success 200
// @Failure 400 {object} stringResponse
// @Failure 500 {object} stringResponse
// @Router /invites/notify [post]
func (app *appContext) SetNotify(gc *gin.Context) {
var req map[string]map[string]bool
gc.BindJSON(&req)
@ -639,8 +779,7 @@ func (app *appContext) SetNotify(gc *gin.Context) {
invite, ok := app.storage.invites[code]
if !ok {
app.err.Printf("%s Notification setting change failed: Invalid code", code)
gc.JSON(400, map[string]string{"error": "Invalid invite code"})
gc.Abort()
respond(400, "Invalid invite code", gc)
return
}
var address string
@ -650,8 +789,7 @@ func (app *appContext) SetNotify(gc *gin.Context) {
if !ok {
app.err.Printf("%s: Couldn't find email address. Make sure it's set", code)
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
gc.JSON(500, map[string]string{"error": "Missing user email"})
gc.Abort()
respond(500, "Missing user email", gc)
return
}
} else {
@ -684,12 +822,18 @@ func (app *appContext) SetNotify(gc *gin.Context) {
}
}
type deleteReq struct {
Code string `json:"code"`
type deleteInviteDTO struct {
Code string `json:"code" example:"skjadajd43234s"` // Code of invite to delete
}
// @Summary Delete an invite.
// @Produce json
// @Param deleteInviteDTO body deleteInviteDTO true "Delete invite object"
// @Success 200 {object} boolResponse
// @Failure 400 {object} stringResponse
// @Router /invites [delete]
func (app *appContext) DeleteInvite(gc *gin.Context) {
var req deleteReq
var req deleteInviteDTO
gc.BindJSON(&req)
app.debug.Printf("%s: Deletion requested", req.Code)
var ok bool
@ -698,11 +842,11 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
delete(app.storage.invites, req.Code)
app.storage.storeInvites()
app.info.Printf("%s: Invite deleted", req.Code)
gc.JSON(200, map[string]bool{"success": true})
respondBool(200, true, gc)
return
}
app.err.Printf("%s: Deletion failed: Invalid code", req.Code)
respond(401, "Code doesn't exist", gc)
respond(400, "Code doesn't exist", gc)
}
type dateToParse struct {
@ -718,21 +862,25 @@ func parseDt(date string) time.Time {
}
type respUser struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
// this magically parses a string to time.time
LastActive string `json:"last_active"`
Admin bool `json:"admin"`
ID string `json:"id" example:"fdgsdfg45534fa"` // userID of user
Name string `json:"name" example:"jeff"` // Username of user
Email string `json:"email,omitempty" example:"jeff@jellyf.in"` // Email address of user (if available)
LastActive string `json:"last_active"` // Time of last activity on Jellyfin
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
}
type userResp struct {
type getUsersDTO struct {
UserList []respUser `json:"users"`
}
// @Summary Get a list of Jellyfin users.
// @Produce json
// @Success 200 {object} getUsersDTO
// @Failure 500 {object} stringResponse
// @Router /users [get]
func (app *appContext) GetUsers(gc *gin.Context) {
app.debug.Println("Users requested")
var resp userResp
var resp getUsersDTO
resp.UserList = []respUser{}
users, status, err := app.jf.getUsers(false)
if !(status == 200 || status == 204) || err != nil {
@ -761,10 +909,19 @@ func (app *appContext) GetUsers(gc *gin.Context) {
}
type ombiUser struct {
Name string `json:"name,omitempty"`
ID string `json:"id"`
Name string `json:"name,omitempty" example:"jeff"` // Name of Ombi user
ID string `json:"id" example:"djgkjdg7dkjfsj8"` // userID of Ombi user
}
type ombiUsersDTO struct {
Users []ombiUser `json:"users"`
}
// @Summary Get a list of Ombi users.
// @Produce json
// @Success 200 {object} ombiUsersDTO
// @Failure 500 {object} stringResponse
// @Router /ombi/users [get]
func (app *appContext) OmbiUsers(gc *gin.Context) {
app.debug.Println("Ombi users requested")
users, status, err := app.ombi.getUsers()
@ -781,11 +938,40 @@ func (app *appContext) OmbiUsers(gc *gin.Context) {
ID: data["id"].(string),
}
}
gc.JSON(200, map[string][]ombiUser{"users": userlist})
gc.JSON(200, ombiUsersDTO{Users: userlist})
}
// @Summary Set new user defaults for Ombi accounts.
// @Produce json
// @Param ombiUser body ombiUser true "User to source settings from"
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /ombi/defaults [post]
func (app *appContext) SetOmbiDefaults(gc *gin.Context) {
var req ombiUser
gc.BindJSON(&req)
template, code, err := app.ombi.templateByID(req.ID)
if err != nil || code != 200 || len(template) == 0 {
app.err.Printf("Couldn't get user from Ombi: %d %s", code, err)
respond(500, "Couldn't get user", gc)
return
}
app.storage.ombi_template = template
fmt.Println(app.storage.ombi_path)
app.storage.storeOmbiTemplate()
respondBool(200, true, gc)
}
type modifyEmailsDTO map[string]string
// @Summary Modify user's email addresses.
// @Produce json
// @Param modifyEmailsDTO body modifyEmailsDTO true "Map of userIDs to email addresses"
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /users/emails [post]
func (app *appContext) ModifyEmails(gc *gin.Context) {
var req map[string]string
var req modifyEmailsDTO
gc.BindJSON(&req)
fmt.Println(req)
app.debug.Println("Email modification requested")
@ -803,30 +989,7 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
}
app.storage.storeEmails()
app.info.Println("Email list modified")
gc.JSON(200, map[string]bool{"success": true})
}
func (app *appContext) SetOmbiDefaults(gc *gin.Context) {
var req ombiUser
gc.BindJSON(&req)
template, code, err := app.ombi.templateByID(req.ID)
if err != nil || code != 200 || len(template) == 0 {
app.err.Printf("Couldn't get user from Ombi: %d %s", code, err)
respond(500, "Couldn't get user", gc)
return
}
app.storage.ombi_template = template
fmt.Println(app.storage.ombi_path)
app.storage.storeOmbiTemplate()
gc.JSON(200, map[string]bool{"success": true})
}
type defaultsReq struct {
From string `json:"from"`
Profile string `json:"profile"`
ApplyTo []string `json:"apply_to"`
ID string `json:"id"`
Homescreen bool `json:"homescreen"`
respondBool(200, true, gc)
}
/*func (app *appContext) SetDefaults(gc *gin.Context) {
@ -865,9 +1028,25 @@ type defaultsReq struct {
gc.JSON(200, map[string]bool{"success": true})
}*/
type userSettingsDTO struct {
From string `json:"from"` // Whether to apply from "user" or "profile"
Profile string `json:"profile"` // Name of profile (if from = "profile")
ApplyTo []string `json:"apply_to"` // Users to apply settings to
ID string `json:"id"` // ID of user (if from = "user")
Homescreen bool `json:"homescreen"` // Whether to apply homescreen layout or not
}
type errorListDTO map[string]map[string]string
// @Summary Apply settings to a list of users, either from a profile or from another user.
// @Produce json
// @Param userSettingsDTO body userSettingsDTO true "Parameters for applying settings"
// @Success 200 {object} errorListDTO
// @Failure 500 {object} errorListDTO "Lists of errors that occured while applying settings"
// @Router /users/settings [post]
func (app *appContext) ApplySettings(gc *gin.Context) {
app.info.Println("User settings change requested")
var req defaultsReq
var req userSettingsDTO
gc.BindJSON(&req)
applyingFrom := "profile"
var policy, configuration, displayprefs map[string]interface{}
@ -911,7 +1090,7 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
}
}
app.info.Printf("Applying settings to %d user(s) from %s", len(req.ApplyTo), applyingFrom)
errors := map[string]map[string]string{
errors := errorListDTO{
"policy": map[string]string{},
"homescreen": map[string]string{},
}
@ -943,6 +1122,10 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
gc.JSON(code, errors)
}
// @Summary Get jfa-go configuration.
// @Produce json
// @Success 200 {object} configDTO "Uses the same format as config-base.json"
// @Router /config [get]
func (app *appContext) GetConfig(gc *gin.Context) {
app.info.Println("Config requested")
resp := map[string]interface{}{}
@ -976,9 +1159,17 @@ func (app *appContext) GetConfig(gc *gin.Context) {
gc.JSON(200, resp)
}
// @Summary Modify app config.
// @Produce json
// @Param appConfig body configDTO true "Config split into sections as in config.ini, all values as strings."
// @Success 200 {object} boolResponse
// @Router /config [post]
type configDTO map[string]interface{}
func (app *appContext) ModifyConfig(gc *gin.Context) {
app.info.Println("Config modification requested")
var req map[string]interface{}
var req configDTO
gc.BindJSON(&req)
tempConfig, _ := ini.Load(app.config_path)
for section, settings := range req {
@ -1022,6 +1213,11 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
}
}
// @Summary Logout by deleting refresh token from cookies.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /logout [post]
func (app *appContext) Logout(gc *gin.Context) {
cookie, err := gc.Cookie("refresh")
if err != nil {
@ -1031,7 +1227,7 @@ func (app *appContext) Logout(gc *gin.Context) {
}
app.invalidTokens = append(app.invalidTokens, cookie)
gc.SetCookie("refresh", "invalid", -1, "/", gc.Request.URL.Hostname(), true, true)
gc.JSON(200, map[string]bool{"success": true})
respondBool(200, true, gc)
}
// func Restart() error {

11
auth.go
View File

@ -43,17 +43,6 @@ func CreateToken(userId, jfId string) (string, string, error) {
return token, refresh, nil
}
func respond(code int, message string, gc *gin.Context) {
resp := map[string]string{}
if code == 200 || code == 204 {
resp["response"] = message
} else {
resp["error"] = message
}
gc.JSON(code, resp)
gc.Abort()
}
// Check header for token
func (app *appContext) authenticate(gc *gin.Context) {
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)

1062
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

3
docs/go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/hrfee/jfa-go/docs
go 1.15

999
docs/swagger.json Normal file
View File

@ -0,0 +1,999 @@
{
"swagger": "2.0",
"info": {
"description": "API for the jfa-go frontend",
"title": "jfa-go internal API",
"contact": {
"name": "Harvey Tindall",
"email": "hrfee@protonmail.ch"
},
"license": {
"name": "MIT",
"url": "https://raw.githubusercontent.com/hrfee/jfa-go/main/LICENSE"
},
"version": "0.2.0"
},
"basePath": "/",
"paths": {
"/config": {
"get": {
"produces": [
"application/json"
],
"summary": "Get jfa-go configuration.",
"responses": {
"200": {
"description": "Uses the same format as config-base.json",
"schema": {
"$ref": "#/definitions/main.configDTO"
}
}
}
}
},
"/invites": {
"get": {
"produces": [
"application/json"
],
"summary": "Get invites.",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.getInvitesDTO"
}
}
}
},
"post": {
"produces": [
"application/json"
],
"summary": "Create a new invite.",
"parameters": [
{
"description": "New invite request object",
"name": "generateInviteDTO",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.generateInviteDTO"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.boolResponse"
}
}
}
},
"delete": {
"produces": [
"application/json"
],
"summary": "Delete an invite.",
"parameters": [
{
"description": "Delete invite object",
"name": "deleteInviteDTO",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.deleteInviteDTO"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.boolResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/main.stringResponse"
}
}
}
}
},
"/invites/notify": {
"post": {
"produces": [
"application/json"
],
"summary": "Set notification preferences for an invite.",
"parameters": [
{
"description": "Map of invite codes to notification settings objects",
"name": "setNotifyDTO",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.setNotifyDTO"
}
}
],
"responses": {
"200": {},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/main.stringResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/main.stringResponse"
}
}
}
}
},
"/invites/profile": {
"post": {
"produces": [
"application/json"
],
"summary": "Set profile for an invite",
"parameters": [
{
"description": "Invite profile object",
"name": "inviteProfileDTO",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.inviteProfileDTO"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.boolResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/main.stringResponse"
}
}
}
}
},
"/logout": {
"post": {
"produces": [
"application/json"
],
"summary": "Logout by deleting refresh token from cookies.",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.boolResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/main.stringResponse"
}
}
}
}
},
"/newUser": {
"post": {
"produces": [
"application/json"
],
"summary": "Creates a new Jellyfin user via invite code",
"parameters": [
{
"description": "New user request object",
"name": "newUserDTO",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.newUserDTO"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.PasswordValidation"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/main.PasswordValidation"
}
}
}
}
},
"/ombi/defaults": {
"post": {
"produces": [
"application/json"
],
"summary": "Set new user defaults for Ombi accounts.",
"parameters": [
{
"description": "User to source settings from",
"name": "ombiUser",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.ombiUser"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.boolResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/main.stringResponse"
}
}
}
}
},
"/ombi/users": {
"get": {
"produces": [
"application/json"
],
"summary": "Get a list of Ombi users.",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.ombiUsersDTO"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/main.stringResponse"
}
}
}
}
},
"/profiles": {
"get": {
"produces": [
"application/json"
],
"summary": "Get a list of profiles",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.getProfilesDTO"
}
}
}
},
"post": {
"produces": [
"application/json"
],
"summary": "Create a profile based on a Jellyfin user's settings.",
"parameters": [
{
"description": "New profile object",
"name": "newProfileDTO",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.newProfileDTO"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.boolResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/main.stringResponse"
}
}
}
},
"delete": {
"produces": [
"application/json"
],
"summary": "Delete an existing profile",
"parameters": [
{
"description": "Delete profile object",
"name": "profileChangeDTO",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.profileChangeDTO"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.boolResponse"
}
}
}
}
},
"/profiles/default": {
"post": {
"produces": [
"application/json"
],
"summary": "Set the default profile to use.",
"parameters": [
{
"description": "Default profile object",
"name": "profileChangeDTO",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.profileChangeDTO"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.boolResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/main.stringResponse"
}
}
}
}
},
"/users": {
"get": {
"produces": [
"application/json"
],
"summary": "Get a list of Jellyfin users.",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.getUsersDTO"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/main.stringResponse"
}
}
}
},
"post": {
"produces": [
"application/json"
],
"summary": "Creates a new Jellyfin user without an invite.",
"parameters": [
{
"description": "New user request object",
"name": "newUserDTO",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.newUserDTO"
}
}
],
"responses": {
"200": {}
}
},
"delete": {
"produces": [
"application/json"
],
"summary": "Delete a list of users, optionally notifying them why.",
"parameters": [
{
"description": "User deletion request object",
"name": "deleteUserDTO",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.deleteUserDTO"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.boolResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/main.stringResponse"
}
},
"500": {
"description": "List of errors",
"schema": {
"$ref": "#/definitions/main.errorListDTO"
}
}
}
}
},
"/users/emails": {
"post": {
"produces": [
"application/json"
],
"summary": "Modify user's email addresses.",
"parameters": [
{
"description": "Map of userIDs to email addresses",
"name": "modifyEmailsDTO",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.modifyEmailsDTO"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.boolResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/main.stringResponse"
}
}
}
}
},
"/users/settings": {
"post": {
"produces": [
"application/json"
],
"summary": "Apply settings to a list of users, either from a profile or from another user.",
"parameters": [
{
"description": "Parameters for applying settings",
"name": "userSettingsDTO",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.userSettingsDTO"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.errorListDTO"
}
},
"500": {
"description": "Lists of errors that occured while applying settings",
"schema": {
"$ref": "#/definitions/main.errorListDTO"
}
}
}
}
}
},
"definitions": {
"main.PasswordValidation": {
"type": "object",
"properties": {
"characters": {
"description": "Number of characters",
"type": "boolean"
},
"lowercase characters": {
"description": "Number of lowercase characters",
"type": "boolean"
},
"numbers": {
"description": "Number of numbers",
"type": "boolean"
},
"special characters": {
"description": "Number of special characters",
"type": "boolean"
},
"uppercase characters": {
"description": "Number of uppercase characters",
"type": "boolean"
}
}
},
"main.boolResponse": {
"type": "object",
"properties": {
"error": {
"type": "boolean",
"example": true
},
"success": {
"type": "boolean",
"example": false
}
}
},
"main.configDTO": {
"type": "object",
"additionalProperties": true
},
"main.deleteInviteDTO": {
"type": "object",
"properties": {
"code": {
"description": "Code of invite to delete",
"type": "string",
"example": "skjadajd43234s"
}
}
},
"main.deleteUserDTO": {
"type": "object",
"required": [
"users"
],
"properties": {
"notify": {
"description": "Whether to notify users of deletion",
"type": "boolean"
},
"reason": {
"description": "Account deletion reason (for notification)",
"type": "string"
},
"users": {
"description": "List of usernames to delete",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"main.errorListDTO": {
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"main.generateInviteDTO": {
"type": "object",
"properties": {
"days": {
"description": "Number of days",
"type": "integer",
"example": 1
},
"email": {
"description": "Send invite to this address",
"type": "string",
"example": "jeff@jellyf.in"
},
"hours": {
"description": "Number of hours",
"type": "integer",
"example": 2
},
"minutes": {
"description": "Number of minutes",
"type": "integer",
"example": 3
},
"multiple-uses": {
"description": "Allow multiple uses",
"type": "boolean",
"example": true
},
"no-limit": {
"description": "No invite use limit",
"type": "boolean",
"example": false
},
"profile": {
"description": "Name of profile to apply on this invite",
"type": "string",
"example": "DefaultProfile"
},
"remaining-uses": {
"description": "Remaining invite uses",
"type": "integer",
"example": 5
}
}
},
"main.getInvitesDTO": {
"type": "object",
"properties": {
"invites": {
"description": "List of invites",
"type": "array",
"items": {
"$ref": "#/definitions/main.inviteDTO"
}
},
"profiles": {
"description": "List of profiles (name only)",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"main.getProfilesDTO": {
"type": "object",
"properties": {
"default_profile": {
"type": "string"
},
"profiles": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/main.profileDTO"
}
}
}
},
"main.getUsersDTO": {
"type": "object",
"properties": {
"users": {
"type": "array",
"items": {
"$ref": "#/definitions/main.respUser"
}
}
}
},
"main.inviteDTO": {
"type": "object",
"properties": {
"code": {
"description": "Invite code",
"type": "string",
"example": "sajdlj23423j23"
},
"created": {
"description": "Date of creation",
"type": "string",
"example": "01/01/20 12:00"
},
"days": {
"description": "Number of days till expiry",
"type": "integer",
"example": 1
},
"email": {
"description": "Email the invite was sent to (if applicable)",
"type": "string"
},
"hours": {
"description": "Number of hours till expiry",
"type": "integer",
"example": 2
},
"minutes": {
"description": "Number of minutes till expiry",
"type": "integer",
"example": 3
},
"no-limit": {
"description": "If true, invite can be used any number of times",
"type": "boolean"
},
"notify-creation": {
"description": "Whether to notify the requesting user of account creation or not",
"type": "boolean"
},
"notify-expiry": {
"description": "Whether to notify the requesting user of expiry or not",
"type": "boolean"
},
"profile": {
"description": "Profile used on this invite",
"type": "string",
"example": "DefaultProfile"
},
"remaining-uses": {
"description": "Remaining number of uses (if applicable)",
"type": "integer"
},
"used-by": {
"description": "Users who have used this invite",
"type": "array",
"items": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"main.inviteProfileDTO": {
"type": "object",
"properties": {
"invite": {
"description": "Invite to apply to",
"type": "string",
"example": "slakdaslkdl2342"
},
"profile": {
"description": "Profile to use",
"type": "string",
"example": "DefaultProfile"
}
}
},
"main.modifyEmailsDTO": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"main.newProfileDTO": {
"type": "object",
"required": [
"id",
"name"
],
"properties": {
"homescreen": {
"description": "Whether to store homescreen layout or not",
"type": "boolean",
"example": true
},
"id": {
"description": "ID of user to source settings from",
"type": "string",
"example": "kasdjlaskjd342342"
},
"name": {
"description": "Name of the profile",
"type": "string",
"example": "DefaultProfile"
}
}
},
"main.newUserDTO": {
"type": "object",
"required": [
"password",
"username"
],
"properties": {
"code": {
"description": "Invite code (required on /newUser)",
"type": "string",
"example": "abc0933jncjkcjj"
},
"email": {
"description": "User's email address",
"type": "string",
"example": "jeff@jellyf.in"
},
"password": {
"description": "User's password",
"type": "string",
"example": "guest"
},
"username": {
"description": "User's username",
"type": "string",
"example": "jeff"
}
}
},
"main.ombiUser": {
"type": "object",
"properties": {
"id": {
"description": "userID of Ombi user",
"type": "string",
"example": "djgkjdg7dkjfsj8"
},
"name": {
"description": "Name of Ombi user",
"type": "string",
"example": "jeff"
}
}
},
"main.ombiUsersDTO": {
"type": "object",
"properties": {
"users": {
"type": "array",
"items": {
"$ref": "#/definitions/main.ombiUser"
}
}
}
},
"main.profileChangeDTO": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"description": "Name of the profile",
"type": "string",
"example": "DefaultProfile"
}
}
},
"main.profileDTO": {
"type": "object",
"properties": {
"admin": {
"description": "Whether profile has admin rights or not",
"type": "boolean",
"example": false
},
"fromUser": {
"description": "The user the profile is based on",
"type": "string",
"example": "jeff"
},
"libraries": {
"description": "Number of libraries profile has access to",
"type": "string",
"example": "all"
}
}
},
"main.respUser": {
"type": "object",
"properties": {
"admin": {
"description": "Whether or not the user is Administrator",
"type": "boolean",
"example": false
},
"email": {
"description": "Email address of user (if available)",
"type": "string",
"example": "jeff@jellyf.in"
},
"id": {
"description": "userID of user",
"type": "string",
"example": "fdgsdfg45534fa"
},
"last_active": {
"description": "Time of last activity on Jellyfin",
"type": "string"
},
"name": {
"description": "Username of user",
"type": "string",
"example": "jeff"
}
}
},
"main.setNotifyDTO": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/main.setNotifyValues"
}
},
"main.setNotifyValues": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"notify-creation": {
"description": "Whether to notify the requesting user of account creation or not",
"type": "boolean"
},
"notify-expiry": {
"description": "Whether to notify the requesting user of expiry or not",
"type": "boolean"
}
}
}
},
"main.stringResponse": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "errorDescription"
},
"response": {
"type": "string",
"example": "message"
}
}
},
"main.userSettingsDTO": {
"type": "object",
"properties": {
"apply_to": {
"description": "Users to apply settings to",
"type": "array",
"items": {
"type": "string"
}
},
"from": {
"description": "Whether to apply from \"user\" or \"profile\"",
"type": "string"
},
"homescreen": {
"description": "Whether to apply homescreen layout or not",
"type": "boolean"
},
"id": {
"description": "ID of user (if from = \"user\")",
"type": "string"
},
"profile": {
"description": "Name of profile (if from = \"profile\")",
"type": "string"
}
}
}
}
}

678
docs/swagger.yaml Normal file
View File

@ -0,0 +1,678 @@
basePath: /
definitions:
main.PasswordValidation:
properties:
characters:
description: Number of characters
type: boolean
lowercase characters:
description: Number of lowercase characters
type: boolean
numbers:
description: Number of numbers
type: boolean
special characters:
description: Number of special characters
type: boolean
uppercase characters:
description: Number of uppercase characters
type: boolean
type: object
main.boolResponse:
properties:
error:
example: true
type: boolean
success:
example: false
type: boolean
type: object
main.configDTO:
additionalProperties: true
type: object
main.deleteInviteDTO:
properties:
code:
description: Code of invite to delete
example: skjadajd43234s
type: string
type: object
main.deleteUserDTO:
properties:
notify:
description: Whether to notify users of deletion
type: boolean
reason:
description: Account deletion reason (for notification)
type: string
users:
description: List of usernames to delete
items:
type: string
type: array
required:
- users
type: object
main.errorListDTO:
additionalProperties:
additionalProperties:
type: string
type: object
type: object
main.generateInviteDTO:
properties:
days:
description: Number of days
example: 1
type: integer
email:
description: Send invite to this address
example: jeff@jellyf.in
type: string
hours:
description: Number of hours
example: 2
type: integer
minutes:
description: Number of minutes
example: 3
type: integer
multiple-uses:
description: Allow multiple uses
example: true
type: boolean
no-limit:
description: No invite use limit
example: false
type: boolean
profile:
description: Name of profile to apply on this invite
example: DefaultProfile
type: string
remaining-uses:
description: Remaining invite uses
example: 5
type: integer
type: object
main.getInvitesDTO:
properties:
invites:
description: List of invites
items:
$ref: '#/definitions/main.inviteDTO'
type: array
profiles:
description: List of profiles (name only)
items:
type: string
type: array
type: object
main.getProfilesDTO:
properties:
default_profile:
type: string
profiles:
additionalProperties:
$ref: '#/definitions/main.profileDTO'
type: object
type: object
main.getUsersDTO:
properties:
users:
items:
$ref: '#/definitions/main.respUser'
type: array
type: object
main.inviteDTO:
properties:
code:
description: Invite code
example: sajdlj23423j23
type: string
created:
description: Date of creation
example: 01/01/20 12:00
type: string
days:
description: Number of days till expiry
example: 1
type: integer
email:
description: Email the invite was sent to (if applicable)
type: string
hours:
description: Number of hours till expiry
example: 2
type: integer
minutes:
description: Number of minutes till expiry
example: 3
type: integer
no-limit:
description: If true, invite can be used any number of times
type: boolean
notify-creation:
description: Whether to notify the requesting user of account creation or not
type: boolean
notify-expiry:
description: Whether to notify the requesting user of expiry or not
type: boolean
profile:
description: Profile used on this invite
example: DefaultProfile
type: string
remaining-uses:
description: Remaining number of uses (if applicable)
type: integer
used-by:
description: Users who have used this invite
items:
items:
type: string
type: array
type: array
type: object
main.inviteProfileDTO:
properties:
invite:
description: Invite to apply to
example: slakdaslkdl2342
type: string
profile:
description: Profile to use
example: DefaultProfile
type: string
type: object
main.modifyEmailsDTO:
additionalProperties:
type: string
type: object
main.newProfileDTO:
properties:
homescreen:
description: Whether to store homescreen layout or not
example: true
type: boolean
id:
description: ID of user to source settings from
example: kasdjlaskjd342342
type: string
name:
description: Name of the profile
example: DefaultProfile
type: string
required:
- id
- name
type: object
main.newUserDTO:
properties:
code:
description: Invite code (required on /newUser)
example: abc0933jncjkcjj
type: string
email:
description: User's email address
example: jeff@jellyf.in
type: string
password:
description: User's password
example: guest
type: string
username:
description: User's username
example: jeff
type: string
required:
- password
- username
type: object
main.ombiUser:
properties:
id:
description: userID of Ombi user
example: djgkjdg7dkjfsj8
type: string
name:
description: Name of Ombi user
example: jeff
type: string
type: object
main.ombiUsersDTO:
properties:
users:
items:
$ref: '#/definitions/main.ombiUser'
type: array
type: object
main.profileChangeDTO:
properties:
name:
description: Name of the profile
example: DefaultProfile
type: string
required:
- name
type: object
main.profileDTO:
properties:
admin:
description: Whether profile has admin rights or not
example: false
type: boolean
fromUser:
description: The user the profile is based on
example: jeff
type: string
libraries:
description: Number of libraries profile has access to
example: all
type: string
type: object
main.respUser:
properties:
admin:
description: Whether or not the user is Administrator
example: false
type: boolean
email:
description: Email address of user (if available)
example: jeff@jellyf.in
type: string
id:
description: userID of user
example: fdgsdfg45534fa
type: string
last_active:
description: Time of last activity on Jellyfin
type: string
name:
description: Username of user
example: jeff
type: string
type: object
main.setNotifyDTO:
additionalProperties:
$ref: '#/definitions/main.setNotifyValues'
type: object
main.setNotifyValues:
additionalProperties:
properties:
notify-creation:
description: Whether to notify the requesting user of account creation or not
type: boolean
notify-expiry:
description: Whether to notify the requesting user of expiry or not
type: boolean
type: object
type: object
main.stringResponse:
properties:
error:
example: errorDescription
type: string
response:
example: message
type: string
type: object
main.userSettingsDTO:
properties:
apply_to:
description: Users to apply settings to
items:
type: string
type: array
from:
description: Whether to apply from "user" or "profile"
type: string
homescreen:
description: Whether to apply homescreen layout or not
type: boolean
id:
description: ID of user (if from = "user")
type: string
profile:
description: Name of profile (if from = "profile")
type: string
type: object
info:
contact:
email: hrfee@protonmail.ch
name: Harvey Tindall
description: API for the jfa-go frontend
license:
name: MIT
url: https://raw.githubusercontent.com/hrfee/jfa-go/main/LICENSE
title: jfa-go internal API
version: 0.2.0
paths:
/config:
get:
produces:
- application/json
responses:
"200":
description: Uses the same format as config-base.json
schema:
$ref: '#/definitions/main.configDTO'
summary: Get jfa-go configuration.
/invites:
delete:
parameters:
- description: Delete invite object
in: body
name: deleteInviteDTO
required: true
schema:
$ref: '#/definitions/main.deleteInviteDTO'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.boolResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/main.stringResponse'
summary: Delete an invite.
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.getInvitesDTO'
summary: Get invites.
post:
parameters:
- description: New invite request object
in: body
name: generateInviteDTO
required: true
schema:
$ref: '#/definitions/main.generateInviteDTO'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.boolResponse'
summary: Create a new invite.
/invites/notify:
post:
parameters:
- description: Map of invite codes to notification settings objects
in: body
name: setNotifyDTO
required: true
schema:
$ref: '#/definitions/main.setNotifyDTO'
produces:
- application/json
responses:
"200": {}
"400":
description: Bad Request
schema:
$ref: '#/definitions/main.stringResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/main.stringResponse'
summary: Set notification preferences for an invite.
/invites/profile:
post:
parameters:
- description: Invite profile object
in: body
name: inviteProfileDTO
required: true
schema:
$ref: '#/definitions/main.inviteProfileDTO'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.boolResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/main.stringResponse'
summary: Set profile for an invite
/logout:
post:
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.boolResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/main.stringResponse'
summary: Logout by deleting refresh token from cookies.
/newUser:
post:
parameters:
- description: New user request object
in: body
name: newUserDTO
required: true
schema:
$ref: '#/definitions/main.newUserDTO'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.PasswordValidation'
"400":
description: Bad Request
schema:
$ref: '#/definitions/main.PasswordValidation'
summary: Creates a new Jellyfin user via invite code
/ombi/defaults:
post:
parameters:
- description: User to source settings from
in: body
name: ombiUser
required: true
schema:
$ref: '#/definitions/main.ombiUser'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.boolResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/main.stringResponse'
summary: Set new user defaults for Ombi accounts.
/ombi/users:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.ombiUsersDTO'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/main.stringResponse'
summary: Get a list of Ombi users.
/profiles:
delete:
parameters:
- description: Delete profile object
in: body
name: profileChangeDTO
required: true
schema:
$ref: '#/definitions/main.profileChangeDTO'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.boolResponse'
summary: Delete an existing profile
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.getProfilesDTO'
summary: Get a list of profiles
post:
parameters:
- description: New profile object
in: body
name: newProfileDTO
required: true
schema:
$ref: '#/definitions/main.newProfileDTO'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.boolResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/main.stringResponse'
summary: Create a profile based on a Jellyfin user's settings.
/profiles/default:
post:
parameters:
- description: Default profile object
in: body
name: profileChangeDTO
required: true
schema:
$ref: '#/definitions/main.profileChangeDTO'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.boolResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/main.stringResponse'
summary: Set the default profile to use.
/users:
delete:
parameters:
- description: User deletion request object
in: body
name: deleteUserDTO
required: true
schema:
$ref: '#/definitions/main.deleteUserDTO'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.boolResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/main.stringResponse'
"500":
description: List of errors
schema:
$ref: '#/definitions/main.errorListDTO'
summary: Delete a list of users, optionally notifying them why.
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.getUsersDTO'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/main.stringResponse'
summary: Get a list of Jellyfin users.
post:
parameters:
- description: New user request object
in: body
name: newUserDTO
required: true
schema:
$ref: '#/definitions/main.newUserDTO'
produces:
- application/json
responses:
"200": {}
summary: Creates a new Jellyfin user without an invite.
/users/emails:
post:
parameters:
- description: Map of userIDs to email addresses
in: body
name: modifyEmailsDTO
required: true
schema:
$ref: '#/definitions/main.modifyEmailsDTO'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.boolResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/main.stringResponse'
summary: Modify user's email addresses.
/users/settings:
post:
parameters:
- description: Parameters for applying settings
in: body
name: userSettingsDTO
required: true
schema:
$ref: '#/definitions/main.userSettingsDTO'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.errorListDTO'
"500":
description: Lists of errors that occured while applying settings
schema:
$ref: '#/definitions/main.errorListDTO'
summary: Apply settings to a list of users, either from a profile or from another user.
swagger: "2.0"

15
go.mod
View File

@ -2,25 +2,38 @@ module github.com/hrfee/jfa-go
go 1.14
replace github.com/hrfee/jfa-go/docs => ./docs
require (
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/fsnotify/fsnotify v1.4.9
github.com/gin-contrib/pprof v1.3.0
github.com/gin-contrib/static v0.0.0-20200815103939-31fb0c56a3d1
github.com/gin-gonic/gin v1.6.3
github.com/go-chi/chi v4.1.2+incompatible // indirect
github.com/go-openapi/spec v0.19.9 // indirect
github.com/go-openapi/swag v0.19.9 // indirect
github.com/go-playground/validator/v10 v10.3.0 // indirect
github.com/golang/protobuf v1.4.2 // indirect
github.com/hrfee/jfa-go/docs v0.0.0-00010101000000-000000000000
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e
github.com/json-iterator/go v1.1.10 // indirect
github.com/knz/strtime v0.0.0-20200318182718-be999391ffa9
github.com/lithammer/shortuuid/v3 v3.0.4
github.com/mailgun/mailgun-go/v4 v4.1.3
github.com/mailru/easyjson v0.7.3 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pdrum/swagger-automation v0.0.0-20190629163613-c8c7c80ba858
github.com/pkg/errors v0.9.1 // indirect
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14
github.com/swaggo/gin-swagger v1.2.0
github.com/swaggo/swag v1.6.7 // indirect
github.com/urfave/cli/v2 v2.2.0 // indirect
golang.org/x/net v0.0.0-20200923182212-328152dc79b1 // indirect
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect
golang.org/x/tools v0.0.0-20200923182640-463111b69878 // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/ini.v1 v1.60.0
gopkg.in/yaml.v2 v2.3.0 // indirect

138
go.sum
View File

@ -1,7 +1,20 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM=
@ -15,14 +28,21 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqL
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=
github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
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-contrib/static v0.0.0-20191128031702-f81c604d8ac2 h1:xLG16iua01X7Gzms9045s2Y2niNpvSY/Zb1oBwgNYZY=
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2/go.mod h1:VhW/Ch/3FhimwZb8Oj+qJmdMmoB8r7lmJ5auRjm50oQ=
github.com/gin-contrib/static v0.0.0-20200815103939-31fb0c56a3d1 h1:plQYoJeO9lI8Ag0xZy7dDF8FMwIOHsQylKjcclknvIc=
github.com/gin-contrib/static v0.0.0-20200815103939-31fb0c56a3d1/go.mod h1:VhW/Ch/3FhimwZb8Oj+qJmdMmoB8r7lmJ5auRjm50oQ=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
@ -31,6 +51,28 @@ github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuY
github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o=
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
github.com/go-openapi/jsonreference v0.19.4 h1:3Vw+rh13uq2JFNxgnMTGE1rnoieU9FmyE1gvnyylsYg=
github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo=
github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
github.com/go-openapi/spec v0.19.9 h1:9z9cbFuZJ7AcvOHKIY+f6Aevb4vObNDkTEyoMfO7rAc=
github.com/go-openapi/spec v0.19.9/go.mod h1:vqK/dIdLGCosfvYsQV3WfC7N3TiZSnGY2RZKoFK7X28=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.9 h1:1IxuqvBUU3S2Bi4YC7tlP9SJF1gVpCvqN0T2Qof4azE=
github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
@ -45,6 +87,7 @@ github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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=
@ -68,6 +111,8 @@ github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e h1:OGunVjqY7y4
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -75,6 +120,14 @@ github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/knz/strtime v0.0.0-20200318182718-be999391ffa9 h1:GQE1iatYDRrIidq4Zf/9ZzKWyrTk2sXOYc1JADbkAjQ=
github.com/knz/strtime v0.0.0-20200318182718-be999391ffa9/go.mod h1:4ZxfWkxwtc7dBeifERVVWRy9F9rTU9p0yCDgeCtlius=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/gommon v0.2.9 h1:heVeuAYtevIQVYkGj6A41dtfT91LrvFG220lavpWhrU=
github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
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=
@ -86,12 +139,22 @@ github.com/mailgun/mailgun-go v1.1.1 h1:mjMcm4qz+SbjAYbGJ6DKROViKtO5S0YjpuOUxQfd
github.com/mailgun/mailgun-go v2.0.0+incompatible h1:0FoRHWwMUctnd8KIR3vtZbqdfjpIMxOZgcSa51s8F8o=
github.com/mailgun/mailgun-go/v4 v4.1.3 h1:KLa5EZaOMMeyvY/lfAhWxv9ealB3mtUsMz0O9XmTtP0=
github.com/mailgun/mailgun-go/v4 v4.1.3/go.mod h1:R9kHUQBptF4iSEjhriCQizplCDwrnDShy8w/iPiOfaM=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mailru/easyjson v0.7.2 h1:V9ecaZWDYm7v9uJ15RZD6DajMu5sE0hdep0aoDwT9g4=
github.com/mailru/easyjson v0.7.2/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.3 h1:M6wcO9gFHCIPynXGu4iA+NMs//FCgFUWR2jxqV3/+Xk=
github.com/mailru/easyjson v0.7.3/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
@ -103,50 +166,121 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLD
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pdrum/swagger-automation v0.0.0-20190629163613-c8c7c80ba858 h1:lgbJiJQx8bXo+eM88AFdd0VxUvaTLzCBXpK+H9poJ+Y=
github.com/pdrum/swagger-automation v0.0.0-20190629163613-c8c7c80ba858/go.mod h1:y02HeaN0visd95W6cEX2NXDv5sCwyqfzucWTdDGEwYY=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM=
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
github.com/swaggo/gin-swagger v1.2.0 h1:YskZXEiv51fjOMTsXrOetAjrMDfFaXD79PEoQBOe2W0=
github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI=
github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y=
github.com/swaggo/swag v1.6.7 h1:e8GC2xDllJZr3omJkm9YfmK0Y56+rMO3cg0JBKNz09s=
github.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0=
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 v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI=
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/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200923182212-328152dc79b1 h1:Iu68XRPd67wN4aRGGWwwq6bZo/25jR6uu52l/j2KkUE=
golang.org/x/net v0.0.0-20200923182212-328152dc79b1/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIAMvInbeXljJz+jDjeYE=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59 h1:QjA/9ArTfVTLfEhClDCG7SGrZkZixxWpwNCDiwJfh88=
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200923182640-463111b69878 h1:VUw1+Jf6KJPf82mbTQMia6HCnNMv2BbAipkEZ4KTcqQ=
golang.org/x/tools v0.0.0-20200923182640-463111b69878/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@ -167,13 +301,17 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.60.0 h1:P5ZzC7RJO04094NJYlEnBdFK2wwmnCAy/+7sAzvWs60=
gopkg.in/ini.v1 v1.60.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/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=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=

18
main.go
View File

@ -23,7 +23,10 @@ import (
"github.com/gin-contrib/pprof"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
_ "github.com/hrfee/jfa-go/docs"
"github.com/lithammer/shortuuid/v3"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"gopkg.in/ini.v1"
)
@ -108,6 +111,7 @@ var (
PORT *int
DEBUG *bool
TEST bool
SWAGGER *bool
)
func test(app *appContext) {
@ -165,6 +169,7 @@ func start(asDaemon, firstCall bool) {
HOST = flag.String("host", "", "alternate address to host web ui on.")
PORT = flag.Int("port", 0, "alternate port to host web ui on.")
DEBUG = flag.Bool("debug", false, "Enables debug logging and exposes pprof.")
SWAGGER = flag.Bool("swagger", false, "Enable swagger at /swagger/index.html")
flag.Parse()
}
@ -453,6 +458,10 @@ func start(asDaemon, firstCall bool) {
router.POST("/newUser", app.NewUser)
router.Use(static.Serve("/invite/", static.LocalFile(filepath.Join(app.local_path, "static"), false)))
router.GET("/invite/:invCode", app.InviteProxy)
if *SWAGGER {
app.info.Println("WARNING: Swagger should not be used on a public instance.")
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
api := router.Group("/", app.webAuth())
router.POST("/logout", app.Logout)
api.DELETE("/users", app.DeleteUser)
@ -532,6 +541,15 @@ func flagPassed(name string) (found bool) {
return
}
// @title jfa-go internal API
// @version 0.2.0
// @description API for the jfa-go frontend
// @contact.name Harvey Tindall
// @contact.email hrfee@protonmail.ch
// @license.name MIT
// @license.url https://raw.githubusercontent.com/hrfee/jfa-go/main/LICENSE
// @BasePath /
func main() {
fmt.Printf("jfa-go version: %s (%s)\n", VERSION, COMMIT)
folder := "/tmp"

View File

@ -19,9 +19,18 @@ func (vd *Validator) init(criteria ValidatorConf) {
vd.criteria = criteria
}
// This isn't used, its for swagger
type PasswordValidation struct {
Characters bool `json:"characters,omitempty"` // Number of characters
Lowercase bool `json:"lowercase characters,omitempty"` // Number of lowercase characters
Uppercase bool `json:"uppercase characters,omitempty"` // Number of uppercase characters
Numbers bool `json:"numbers,omitempty"` // Number of numbers
Specials bool `json:"special characters,omitempty"` // Number of special characters
}
func (vd *Validator) validate(password string) map[string]bool {
count := map[string]int{}
for key, _ := range vd.criteria {
for key := range vd.criteria {
count[key] = 0
}
for _, c := range password {

View File

@ -144,15 +144,16 @@ const populateProfiles = (noTable?: boolean): void => _get("/profiles", null, fu
const profileList = document.getElementById('profileList');
profileList.textContent = '';
availableProfiles = [this.response["default_profile"]];
for (let name in this.response) {
for (let name in this.response["profiles"]) {
if (name != availableProfiles[0]) {
availableProfiles.push(name);
}
const reqProfile = this.response["profiles"][name];
if (!noTable && name != "default_profile") {
const profile: Profile = {
Admin: this.response[name]["admin"],
LibraryAccess: this.response[name]["libraries"],
FromUser: this.response[name]["fromUser"]
Admin: reqProfile["admin"],
LibraryAccess: reqProfile["libraries"],
FromUser: reqProfile["fromUser"]
};
profileList.innerHTML += `
<td nowrap="nowrap" class="align-middle"><strong>${name}</strong></td>