diff options
Diffstat (limited to 'server/ui/controller')
-rw-r--r-- | server/ui/controller/about.go | 24 | ||||
-rw-r--r-- | server/ui/controller/category.go | 228 | ||||
-rw-r--r-- | server/ui/controller/controller.go | 56 | ||||
-rw-r--r-- | server/ui/controller/entry.go | 375 | ||||
-rw-r--r-- | server/ui/controller/feed.go | 209 | ||||
-rw-r--r-- | server/ui/controller/history.go | 47 | ||||
-rw-r--r-- | server/ui/controller/icon.go | 31 | ||||
-rw-r--r-- | server/ui/controller/login.go | 91 | ||||
-rw-r--r-- | server/ui/controller/opml.go | 63 | ||||
-rw-r--r-- | server/ui/controller/pagination.go | 46 | ||||
-rw-r--r-- | server/ui/controller/proxy.go | 49 | ||||
-rw-r--r-- | server/ui/controller/session.go | 49 | ||||
-rw-r--r-- | server/ui/controller/settings.go | 92 | ||||
-rw-r--r-- | server/ui/controller/static.go | 41 | ||||
-rw-r--r-- | server/ui/controller/subscription.go | 127 | ||||
-rw-r--r-- | server/ui/controller/unread.go | 43 | ||||
-rw-r--r-- | server/ui/controller/user.go | 231 |
17 files changed, 1802 insertions, 0 deletions
diff --git a/server/ui/controller/about.go b/server/ui/controller/about.go new file mode 100644 index 00000000..dcfe0d7a --- /dev/null +++ b/server/ui/controller/about.go @@ -0,0 +1,24 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "github.com/miniflux/miniflux2/server/core" + "github.com/miniflux/miniflux2/version" +) + +func (c *Controller) AboutPage(ctx *core.Context, request *core.Request, response *core.Response) { + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("about", args.Merge(tplParams{ + "version": version.Version, + "build_date": version.BuildDate, + "menu": "settings", + })) +} diff --git a/server/ui/controller/category.go b/server/ui/controller/category.go new file mode 100644 index 00000000..dbc80671 --- /dev/null +++ b/server/ui/controller/category.go @@ -0,0 +1,228 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "errors" + "github.com/miniflux/miniflux2/model" + "github.com/miniflux/miniflux2/server/core" + "github.com/miniflux/miniflux2/server/ui/form" + "log" +) + +func (c *Controller) ShowCategories(ctx *core.Context, request *core.Request, response *core.Response) { + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + user := ctx.GetLoggedUser() + categories, err := c.store.GetCategoriesWithFeedCount(user.ID) + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("categories", args.Merge(tplParams{ + "categories": categories, + "total": len(categories), + "menu": "categories", + })) +} + +func (c *Controller) ShowCategoryEntries(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + offset := request.GetQueryIntegerParam("offset", 0) + + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + category, err := c.getCategoryFromURL(ctx, request, response) + if err != nil { + return + } + + builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithCategoryID(category.ID) + builder.WithOrder(model.DefaultSortingOrder) + builder.WithDirection(model.DefaultSortingDirection) + builder.WithOffset(offset) + builder.WithLimit(NbItemsPerPage) + + entries, err := builder.GetEntries() + if err != nil { + response.Html().ServerError(err) + return + } + + count, err := builder.CountEntries() + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("category_entries", args.Merge(tplParams{ + "category": category, + "entries": entries, + "total": count, + "pagination": c.getPagination(ctx.GetRoute("categoryEntries", "categoryID", category.ID), count, offset), + "menu": "categories", + })) +} + +func (c *Controller) CreateCategory(ctx *core.Context, request *core.Request, response *core.Response) { + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("create_category", args.Merge(tplParams{ + "menu": "categories", + })) +} + +func (c *Controller) SaveCategory(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + categoryForm := form.NewCategoryForm(request.GetRequest()) + if err := categoryForm.Validate(); err != nil { + response.Html().Render("create_category", args.Merge(tplParams{ + "errorMessage": err.Error(), + })) + return + } + + category := model.Category{Title: categoryForm.Title, UserID: user.ID} + err = c.store.CreateCategory(&category) + if err != nil { + log.Println(err) + response.Html().Render("create_category", args.Merge(tplParams{ + "errorMessage": "Unable to create this category.", + })) + return + } + + response.Redirect(ctx.GetRoute("categories")) +} + +func (c *Controller) EditCategory(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + category, err := c.getCategoryFromURL(ctx, request, response) + if err != nil { + log.Println(err) + return + } + + args, err := c.getCategoryFormTemplateArgs(ctx, user, category, nil) + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("edit_category", args) +} + +func (c *Controller) UpdateCategory(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + category, err := c.getCategoryFromURL(ctx, request, response) + if err != nil { + log.Println(err) + return + } + + categoryForm := form.NewCategoryForm(request.GetRequest()) + args, err := c.getCategoryFormTemplateArgs(ctx, user, category, categoryForm) + if err != nil { + response.Html().ServerError(err) + return + } + + if err := categoryForm.Validate(); err != nil { + response.Html().Render("edit_category", args.Merge(tplParams{ + "errorMessage": err.Error(), + })) + return + } + + err = c.store.UpdateCategory(categoryForm.Merge(category)) + if err != nil { + log.Println(err) + response.Html().Render("edit_category", args.Merge(tplParams{ + "errorMessage": "Unable to update this category.", + })) + return + } + + response.Redirect(ctx.GetRoute("categories")) +} + +func (c *Controller) RemoveCategory(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + category, err := c.getCategoryFromURL(ctx, request, response) + if err != nil { + return + } + + if err := c.store.RemoveCategory(user.ID, category.ID); err != nil { + response.Html().ServerError(err) + return + } + + response.Redirect(ctx.GetRoute("categories")) +} + +func (c *Controller) getCategoryFromURL(ctx *core.Context, request *core.Request, response *core.Response) (*model.Category, error) { + categoryID, err := request.GetIntegerParam("categoryID") + if err != nil { + response.Html().BadRequest(err) + return nil, err + } + + user := ctx.GetLoggedUser() + category, err := c.store.GetCategory(user.ID, categoryID) + if err != nil { + response.Html().ServerError(err) + return nil, err + } + + if category == nil { + response.Html().NotFound() + return nil, errors.New("Category not found") + } + + return category, nil +} + +func (c *Controller) getCategoryFormTemplateArgs(ctx *core.Context, user *model.User, category *model.Category, categoryForm *form.CategoryForm) (tplParams, error) { + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + return nil, err + } + + if categoryForm == nil { + args["form"] = form.CategoryForm{ + Title: category.Title, + } + } else { + args["form"] = categoryForm + } + + args["category"] = category + args["menu"] = "categories" + return args, nil +} diff --git a/server/ui/controller/controller.go b/server/ui/controller/controller.go new file mode 100644 index 00000000..aad32582 --- /dev/null +++ b/server/ui/controller/controller.go @@ -0,0 +1,56 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "github.com/miniflux/miniflux2/model" + "github.com/miniflux/miniflux2/reader/feed" + "github.com/miniflux/miniflux2/reader/opml" + "github.com/miniflux/miniflux2/server/core" + "github.com/miniflux/miniflux2/storage" +) + +type tplParams map[string]interface{} + +func (t tplParams) Merge(d tplParams) tplParams { + for k, v := range d { + t[k] = v + } + + return t +} + +type Controller struct { + store *storage.Storage + feedHandler *feed.Handler + opmlHandler *opml.OpmlHandler +} + +func (c *Controller) getCommonTemplateArgs(ctx *core.Context) (tplParams, error) { + user := ctx.GetLoggedUser() + builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithStatus(model.EntryStatusUnread) + + countUnread, err := builder.CountEntries() + if err != nil { + return nil, err + } + + params := tplParams{ + "menu": "", + "user": user, + "countUnread": countUnread, + "csrf": ctx.GetCsrfToken(), + } + return params, nil +} + +func NewController(store *storage.Storage, feedHandler *feed.Handler, opmlHandler *opml.OpmlHandler) *Controller { + return &Controller{ + store: store, + feedHandler: feedHandler, + opmlHandler: opmlHandler, + } +} diff --git a/server/ui/controller/entry.go b/server/ui/controller/entry.go new file mode 100644 index 00000000..5a3a979c --- /dev/null +++ b/server/ui/controller/entry.go @@ -0,0 +1,375 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "errors" + "github.com/miniflux/miniflux2/model" + "github.com/miniflux/miniflux2/server/core" + "github.com/miniflux/miniflux2/server/ui/payload" + "log" +) + +func (c *Controller) ShowFeedEntry(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + sortingDirection := model.DefaultSortingDirection + + entryID, err := request.GetIntegerParam("entryID") + if err != nil { + response.Html().BadRequest(err) + return + } + + feedID, err := request.GetIntegerParam("feedID") + if err != nil { + response.Html().BadRequest(err) + return + } + + builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithFeedID(feedID) + builder.WithEntryID(entryID) + + entry, err := builder.GetEntry() + if err != nil { + response.Html().ServerError(err) + return + } + + if entry == nil { + response.Html().NotFound() + return + } + + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithFeedID(feedID) + builder.WithCondition("e.id", "!=", entryID) + builder.WithCondition("e.published_at", "<=", entry.Date) + builder.WithOrder(model.DefaultSortingOrder) + builder.WithDirection(model.DefaultSortingDirection) + nextEntry, err := builder.GetEntry() + if err != nil { + response.Html().ServerError(err) + return + } + + builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithFeedID(feedID) + builder.WithCondition("e.id", "!=", entryID) + builder.WithCondition("e.published_at", ">=", entry.Date) + builder.WithOrder(model.DefaultSortingOrder) + builder.WithDirection(model.GetOppositeDirection(sortingDirection)) + prevEntry, err := builder.GetEntry() + if err != nil { + response.Html().ServerError(err) + return + } + + nextEntryRoute := "" + if nextEntry != nil { + nextEntryRoute = ctx.GetRoute("feedEntry", "feedID", feedID, "entryID", nextEntry.ID) + } + + prevEntryRoute := "" + if prevEntry != nil { + prevEntryRoute = ctx.GetRoute("feedEntry", "feedID", feedID, "entryID", prevEntry.ID) + } + + if entry.Status == model.EntryStatusUnread { + err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead) + if err != nil { + log.Println(err) + response.Html().ServerError(nil) + return + } + } + + response.Html().Render("entry", args.Merge(tplParams{ + "entry": entry, + "prevEntry": prevEntry, + "nextEntry": nextEntry, + "nextEntryRoute": nextEntryRoute, + "prevEntryRoute": prevEntryRoute, + "menu": "feeds", + })) +} + +func (c *Controller) ShowCategoryEntry(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + sortingDirection := model.DefaultSortingDirection + + categoryID, err := request.GetIntegerParam("categoryID") + if err != nil { + response.Html().BadRequest(err) + return + } + + entryID, err := request.GetIntegerParam("entryID") + if err != nil { + response.Html().BadRequest(err) + return + } + + builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithCategoryID(categoryID) + builder.WithEntryID(entryID) + + entry, err := builder.GetEntry() + if err != nil { + response.Html().ServerError(err) + return + } + + if entry == nil { + response.Html().NotFound() + return + } + + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithCategoryID(categoryID) + builder.WithCondition("e.id", "!=", entryID) + builder.WithCondition("e.published_at", "<=", entry.Date) + builder.WithOrder(model.DefaultSortingOrder) + builder.WithDirection(sortingDirection) + nextEntry, err := builder.GetEntry() + if err != nil { + response.Html().ServerError(err) + return + } + + builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithCategoryID(categoryID) + builder.WithCondition("e.id", "!=", entryID) + builder.WithCondition("e.published_at", ">=", entry.Date) + builder.WithOrder(model.DefaultSortingOrder) + builder.WithDirection(model.GetOppositeDirection(sortingDirection)) + prevEntry, err := builder.GetEntry() + if err != nil { + response.Html().ServerError(err) + return + } + + nextEntryRoute := "" + if nextEntry != nil { + nextEntryRoute = ctx.GetRoute("categoryEntry", "categoryID", categoryID, "entryID", nextEntry.ID) + } + + prevEntryRoute := "" + if prevEntry != nil { + prevEntryRoute = ctx.GetRoute("categoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID) + } + + if entry.Status == model.EntryStatusUnread { + err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead) + if err != nil { + log.Println(err) + response.Html().ServerError(nil) + return + } + } + + response.Html().Render("entry", args.Merge(tplParams{ + "entry": entry, + "prevEntry": prevEntry, + "nextEntry": nextEntry, + "nextEntryRoute": nextEntryRoute, + "prevEntryRoute": prevEntryRoute, + "menu": "categories", + })) +} + +func (c *Controller) ShowUnreadEntry(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + sortingDirection := model.DefaultSortingDirection + + entryID, err := request.GetIntegerParam("entryID") + if err != nil { + response.Html().BadRequest(err) + return + } + + builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithEntryID(entryID) + + entry, err := builder.GetEntry() + if err != nil { + response.Html().ServerError(err) + return + } + + if entry == nil { + response.Html().NotFound() + return + } + + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithStatus(model.EntryStatusUnread) + builder.WithCondition("e.id", "!=", entryID) + builder.WithCondition("e.published_at", "<=", entry.Date) + builder.WithOrder(model.DefaultSortingOrder) + builder.WithDirection(sortingDirection) + nextEntry, err := builder.GetEntry() + if err != nil { + response.Html().ServerError(err) + return + } + + builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithStatus(model.EntryStatusUnread) + builder.WithCondition("e.id", "!=", entryID) + builder.WithCondition("e.published_at", ">=", entry.Date) + builder.WithOrder(model.DefaultSortingOrder) + builder.WithDirection(model.GetOppositeDirection(sortingDirection)) + prevEntry, err := builder.GetEntry() + if err != nil { + response.Html().ServerError(err) + return + } + + nextEntryRoute := "" + if nextEntry != nil { + nextEntryRoute = ctx.GetRoute("unreadEntry", "entryID", nextEntry.ID) + } + + prevEntryRoute := "" + if prevEntry != nil { + prevEntryRoute = ctx.GetRoute("unreadEntry", "entryID", prevEntry.ID) + } + + if entry.Status == model.EntryStatusUnread { + err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead) + if err != nil { + log.Println(err) + response.Html().ServerError(nil) + return + } + } + + response.Html().Render("entry", args.Merge(tplParams{ + "entry": entry, + "prevEntry": prevEntry, + "nextEntry": nextEntry, + "nextEntryRoute": nextEntryRoute, + "prevEntryRoute": prevEntryRoute, + "menu": "unread", + })) +} + +func (c *Controller) ShowReadEntry(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + sortingDirection := model.DefaultSortingDirection + + entryID, err := request.GetIntegerParam("entryID") + if err != nil { + response.Html().BadRequest(err) + return + } + + builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithEntryID(entryID) + + entry, err := builder.GetEntry() + if err != nil { + response.Html().ServerError(err) + return + } + + if entry == nil { + response.Html().NotFound() + return + } + + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithStatus(model.EntryStatusRead) + builder.WithCondition("e.id", "!=", entryID) + builder.WithCondition("e.published_at", "<=", entry.Date) + builder.WithOrder(model.DefaultSortingOrder) + builder.WithDirection(sortingDirection) + nextEntry, err := builder.GetEntry() + if err != nil { + response.Html().ServerError(err) + return + } + + builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithStatus(model.EntryStatusRead) + builder.WithCondition("e.id", "!=", entryID) + builder.WithCondition("e.published_at", ">=", entry.Date) + builder.WithOrder(model.DefaultSortingOrder) + builder.WithDirection(model.GetOppositeDirection(sortingDirection)) + prevEntry, err := builder.GetEntry() + if err != nil { + response.Html().ServerError(err) + return + } + + nextEntryRoute := "" + if nextEntry != nil { + nextEntryRoute = ctx.GetRoute("readEntry", "entryID", nextEntry.ID) + } + + prevEntryRoute := "" + if prevEntry != nil { + prevEntryRoute = ctx.GetRoute("readEntry", "entryID", prevEntry.ID) + } + + response.Html().Render("entry", args.Merge(tplParams{ + "entry": entry, + "prevEntry": prevEntry, + "nextEntry": nextEntry, + "nextEntryRoute": nextEntryRoute, + "prevEntryRoute": prevEntryRoute, + "menu": "history", + })) +} + +func (c *Controller) UpdateEntriesStatus(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + entryIDs, status, err := payload.DecodeEntryStatusPayload(request.GetBody()) + if err != nil { + log.Println(err) + response.Json().BadRequest(nil) + return + } + + if len(entryIDs) == 0 { + response.Html().BadRequest(errors.New("The list of entryID is empty")) + return + } + + err = c.store.SetEntriesStatus(user.ID, entryIDs, status) + if err != nil { + log.Println(err) + response.Html().ServerError(nil) + return + } + + response.Json().Standard("OK") +} diff --git a/server/ui/controller/feed.go b/server/ui/controller/feed.go new file mode 100644 index 00000000..400f81ad --- /dev/null +++ b/server/ui/controller/feed.go @@ -0,0 +1,209 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "errors" + "github.com/miniflux/miniflux2/model" + "github.com/miniflux/miniflux2/server/core" + "github.com/miniflux/miniflux2/server/ui/form" + "log" +) + +func (c *Controller) ShowFeedsPage(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + feeds, err := c.store.GetFeeds(user.ID) + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("feeds", args.Merge(tplParams{ + "feeds": feeds, + "total": len(feeds), + "menu": "feeds", + })) +} + +func (c *Controller) ShowFeedEntries(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + offset := request.GetQueryIntegerParam("offset", 0) + + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + feed, err := c.getFeedFromURL(request, response, user) + if err != nil { + return + } + + builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithFeedID(feed.ID) + builder.WithOrder(model.DefaultSortingOrder) + builder.WithDirection(model.DefaultSortingDirection) + builder.WithOffset(offset) + builder.WithLimit(NbItemsPerPage) + + entries, err := builder.GetEntries() + if err != nil { + response.Html().ServerError(err) + return + } + + count, err := builder.CountEntries() + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("feed_entries", args.Merge(tplParams{ + "feed": feed, + "entries": entries, + "total": count, + "pagination": c.getPagination(ctx.GetRoute("feedEntries", "feedID", feed.ID), count, offset), + "menu": "feeds", + })) +} + +func (c *Controller) EditFeed(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + feed, err := c.getFeedFromURL(request, response, user) + if err != nil { + return + } + + args, err := c.getFeedFormTemplateArgs(ctx, user, feed, nil) + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("edit_feed", args) +} + +func (c *Controller) UpdateFeed(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + feed, err := c.getFeedFromURL(request, response, user) + if err != nil { + return + } + + feedForm := form.NewFeedForm(request.GetRequest()) + args, err := c.getFeedFormTemplateArgs(ctx, user, feed, feedForm) + if err != nil { + response.Html().ServerError(err) + return + } + + if err := feedForm.ValidateModification(); err != nil { + response.Html().Render("edit_feed", args.Merge(tplParams{ + "errorMessage": err.Error(), + })) + return + } + + err = c.store.UpdateFeed(feedForm.Merge(feed)) + if err != nil { + log.Println(err) + response.Html().Render("edit_feed", args.Merge(tplParams{ + "errorMessage": "Unable to update this feed.", + })) + return + } + + response.Redirect(ctx.GetRoute("feeds")) +} + +func (c *Controller) RemoveFeed(ctx *core.Context, request *core.Request, response *core.Response) { + feedID, err := request.GetIntegerParam("feedID") + if err != nil { + response.Html().ServerError(err) + return + } + + user := ctx.GetLoggedUser() + if err := c.store.RemoveFeed(user.ID, feedID); err != nil { + response.Html().ServerError(err) + return + } + + response.Redirect(ctx.GetRoute("feeds")) +} + +func (c *Controller) RefreshFeed(ctx *core.Context, request *core.Request, response *core.Response) { + feedID, err := request.GetIntegerParam("feedID") + if err != nil { + response.Html().BadRequest(err) + return + } + + user := ctx.GetLoggedUser() + if err := c.feedHandler.RefreshFeed(user.ID, feedID); err != nil { + log.Println("[UI:RefreshFeed]", err) + } + + response.Redirect(ctx.GetRoute("feedEntries", "feedID", feedID)) +} + +func (c *Controller) getFeedFromURL(request *core.Request, response *core.Response, user *model.User) (*model.Feed, error) { + feedID, err := request.GetIntegerParam("feedID") + if err != nil { + response.Html().BadRequest(err) + return nil, err + } + + feed, err := c.store.GetFeedById(user.ID, feedID) + if err != nil { + response.Html().ServerError(err) + return nil, err + } + + if feed == nil { + response.Html().NotFound() + return nil, errors.New("Feed not found") + } + + return feed, nil +} + +func (c *Controller) getFeedFormTemplateArgs(ctx *core.Context, user *model.User, feed *model.Feed, feedForm *form.FeedForm) (tplParams, error) { + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + return nil, err + } + + categories, err := c.store.GetCategories(user.ID) + if err != nil { + return nil, err + } + + if feedForm == nil { + args["form"] = form.FeedForm{ + SiteURL: feed.SiteURL, + FeedURL: feed.FeedURL, + Title: feed.Title, + CategoryID: feed.Category.ID, + } + } else { + args["form"] = feedForm + } + + args["categories"] = categories + args["feed"] = feed + args["menu"] = "feeds" + return args, nil +} diff --git a/server/ui/controller/history.go b/server/ui/controller/history.go new file mode 100644 index 00000000..2c067373 --- /dev/null +++ b/server/ui/controller/history.go @@ -0,0 +1,47 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "github.com/miniflux/miniflux2/model" + "github.com/miniflux/miniflux2/server/core" +) + +func (c *Controller) ShowHistoryPage(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + offset := request.GetQueryIntegerParam("offset", 0) + + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithStatus(model.EntryStatusRead) + builder.WithOrder(model.DefaultSortingOrder) + builder.WithDirection(model.DefaultSortingDirection) + builder.WithOffset(offset) + builder.WithLimit(NbItemsPerPage) + + entries, err := builder.GetEntries() + if err != nil { + response.Html().ServerError(err) + return + } + + count, err := builder.CountEntries() + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("history", args.Merge(tplParams{ + "entries": entries, + "total": count, + "pagination": c.getPagination(ctx.GetRoute("history"), count, offset), + "menu": "history", + })) +} diff --git a/server/ui/controller/icon.go b/server/ui/controller/icon.go new file mode 100644 index 00000000..37954c24 --- /dev/null +++ b/server/ui/controller/icon.go @@ -0,0 +1,31 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "github.com/miniflux/miniflux2/server/core" + "time" +) + +func (c *Controller) ShowIcon(ctx *core.Context, request *core.Request, response *core.Response) { + iconID, err := request.GetIntegerParam("iconID") + if err != nil { + response.Html().BadRequest(err) + return + } + + icon, err := c.store.GetIconByID(iconID) + if err != nil { + response.Html().ServerError(err) + return + } + + if icon == nil { + response.Html().NotFound() + return + } + + response.Cache(icon.MimeType, icon.Hash, icon.Content, 72*time.Hour) +} diff --git a/server/ui/controller/login.go b/server/ui/controller/login.go new file mode 100644 index 00000000..225978c1 --- /dev/null +++ b/server/ui/controller/login.go @@ -0,0 +1,91 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "github.com/miniflux/miniflux2/server/core" + "github.com/miniflux/miniflux2/server/ui/form" + "log" + "net/http" + "time" + + "github.com/tomasen/realip" +) + +func (c *Controller) ShowLoginPage(ctx *core.Context, request *core.Request, response *core.Response) { + if ctx.IsAuthenticated() { + response.Redirect(ctx.GetRoute("unread")) + return + } + + response.Html().Render("login", tplParams{ + "csrf": ctx.GetCsrfToken(), + }) +} + +func (c *Controller) CheckLogin(ctx *core.Context, request *core.Request, response *core.Response) { + authForm := form.NewAuthForm(request.GetRequest()) + tplParams := tplParams{ + "errorMessage": "Invalid username or password.", + "csrf": ctx.GetCsrfToken(), + } + + if err := authForm.Validate(); err != nil { + log.Println(err) + response.Html().Render("login", tplParams) + return + } + + if err := c.store.CheckPassword(authForm.Username, authForm.Password); err != nil { + log.Println(err) + response.Html().Render("login", tplParams) + return + } + + sessionToken, err := c.store.CreateSession( + authForm.Username, + request.GetHeaders().Get("User-Agent"), + realip.RealIP(request.GetRequest()), + ) + if err != nil { + response.Html().ServerError(err) + return + } + + log.Printf("[UI:CheckLogin] username=%s just logged in\n", authForm.Username) + + cookie := &http.Cookie{ + Name: "sessionID", + Value: sessionToken, + Path: "/", + Secure: request.IsHTTPS(), + HttpOnly: true, + } + + response.SetCookie(cookie) + response.Redirect(ctx.GetRoute("unread")) +} + +func (c *Controller) Logout(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + sessionCookie := request.GetCookie("sessionID") + if err := c.store.RemoveSessionByToken(user.ID, sessionCookie); err != nil { + log.Printf("[UI:Logout] %v", err) + } + + cookie := &http.Cookie{ + Name: "sessionID", + Value: "", + Path: "/", + Secure: request.IsHTTPS(), + HttpOnly: true, + MaxAge: -1, + Expires: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), + } + + response.SetCookie(cookie) + response.Redirect(ctx.GetRoute("login")) +} diff --git a/server/ui/controller/opml.go b/server/ui/controller/opml.go new file mode 100644 index 00000000..45d34f8e --- /dev/null +++ b/server/ui/controller/opml.go @@ -0,0 +1,63 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "github.com/miniflux/miniflux2/server/core" + "log" +) + +func (c *Controller) Export(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + opml, err := c.opmlHandler.Export(user.ID) + if err != nil { + response.Html().ServerError(err) + return + } + + response.Xml().Download("feeds.opml", opml) +} + +func (c *Controller) Import(ctx *core.Context, request *core.Request, response *core.Response) { + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("import", args.Merge(tplParams{ + "menu": "feeds", + })) +} + +func (c *Controller) UploadOPML(ctx *core.Context, request *core.Request, response *core.Response) { + file, fileHeader, err := request.GetFile("file") + if err != nil { + log.Println(err) + response.Redirect(ctx.GetRoute("import")) + return + } + defer file.Close() + + user := ctx.GetLoggedUser() + log.Printf("[UI:UploadOPML] User #%d uploaded this file: %s (%d bytes)\n", user.ID, fileHeader.Filename, fileHeader.Size) + + if impErr := c.opmlHandler.Import(user.ID, file); impErr != nil { + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("import", args.Merge(tplParams{ + "errorMessage": impErr.Error(), + "menu": "feeds", + })) + + return + } + + response.Redirect(ctx.GetRoute("feeds")) +} diff --git a/server/ui/controller/pagination.go b/server/ui/controller/pagination.go new file mode 100644 index 00000000..b649d900 --- /dev/null +++ b/server/ui/controller/pagination.go @@ -0,0 +1,46 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +const ( + NbItemsPerPage = 100 +) + +type Pagination struct { + Route string + Total int + Offset int + ItemsPerPage int + ShowNext bool + ShowPrev bool + NextOffset int + PrevOffset int +} + +func (c *Controller) getPagination(route string, total, offset int) Pagination { + nextOffset := 0 + prevOffset := 0 + showNext := (total - offset) > NbItemsPerPage + showPrev := offset > 0 + + if showNext { + nextOffset = offset + NbItemsPerPage + } + + if showPrev { + prevOffset = offset - NbItemsPerPage + } + + return Pagination{ + Route: route, + Total: total, + Offset: offset, + ItemsPerPage: NbItemsPerPage, + ShowNext: showNext, + NextOffset: nextOffset, + ShowPrev: showPrev, + PrevOffset: prevOffset, + } +} diff --git a/server/ui/controller/proxy.go b/server/ui/controller/proxy.go new file mode 100644 index 00000000..8a2f2bfa --- /dev/null +++ b/server/ui/controller/proxy.go @@ -0,0 +1,49 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "encoding/base64" + "errors" + "github.com/miniflux/miniflux2/helper" + "github.com/miniflux/miniflux2/server/core" + "io/ioutil" + "log" + "net/http" + "time" +) + +func (c *Controller) ImageProxy(ctx *core.Context, request *core.Request, response *core.Response) { + encodedURL := request.GetStringParam("encodedURL", "") + if encodedURL == "" { + response.Html().BadRequest(errors.New("No URL provided")) + return + } + + decodedURL, err := base64.StdEncoding.DecodeString(encodedURL) + if err != nil { + response.Html().BadRequest(errors.New("Unable to decode this URL")) + return + } + + resp, err := http.Get(string(decodedURL)) + if err != nil { + log.Println(err) + response.Html().NotFound() + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + response.Html().NotFound() + return + } + + body, _ := ioutil.ReadAll(resp.Body) + etag := helper.HashFromBytes(body) + contentType := resp.Header.Get("Content-Type") + + response.Cache(contentType, etag, body, 72*time.Hour) +} diff --git a/server/ui/controller/session.go b/server/ui/controller/session.go new file mode 100644 index 00000000..0255728f --- /dev/null +++ b/server/ui/controller/session.go @@ -0,0 +1,49 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "github.com/miniflux/miniflux2/server/core" + "log" +) + +func (c *Controller) ShowSessions(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + sessions, err := c.store.GetSessions(user.ID) + if err != nil { + response.Html().ServerError(err) + return + } + + sessionCookie := request.GetCookie("sessionID") + response.Html().Render("sessions", args.Merge(tplParams{ + "sessions": sessions, + "currentSessionToken": sessionCookie, + "menu": "settings", + })) +} + +func (c *Controller) RemoveSession(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + sessionID, err := request.GetIntegerParam("sessionID") + if err != nil { + response.Html().BadRequest(err) + return + } + + err = c.store.RemoveSessionByID(user.ID, sessionID) + if err != nil { + log.Println("[UI:RemoveSession]", err) + } + + response.Redirect(ctx.GetRoute("sessions")) +} diff --git a/server/ui/controller/settings.go b/server/ui/controller/settings.go new file mode 100644 index 00000000..a7cca789 --- /dev/null +++ b/server/ui/controller/settings.go @@ -0,0 +1,92 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "github.com/miniflux/miniflux2/locale" + "github.com/miniflux/miniflux2/model" + "github.com/miniflux/miniflux2/server/core" + "github.com/miniflux/miniflux2/server/ui/form" + "log" +) + +func (c *Controller) ShowSettings(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + args, err := c.getSettingsFormTemplateArgs(ctx, user, nil) + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("settings", args) +} + +func (c *Controller) UpdateSettings(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + settingsForm := form.NewSettingsForm(request.GetRequest()) + args, err := c.getSettingsFormTemplateArgs(ctx, user, settingsForm) + if err != nil { + response.Html().ServerError(err) + return + } + + if err := settingsForm.Validate(); err != nil { + response.Html().Render("settings", args.Merge(tplParams{ + "form": settingsForm, + "errorMessage": err.Error(), + })) + return + } + + if c.store.AnotherUserExists(user.ID, settingsForm.Username) { + response.Html().Render("settings", args.Merge(tplParams{ + "form": settingsForm, + "errorMessage": "This user already exists.", + })) + return + } + + err = c.store.UpdateUser(settingsForm.Merge(user)) + if err != nil { + log.Println(err) + response.Html().Render("settings", args.Merge(tplParams{ + "form": settingsForm, + "errorMessage": "Unable to update this user.", + })) + return + } + + response.Redirect(ctx.GetRoute("settings")) +} + +func (c *Controller) getSettingsFormTemplateArgs(ctx *core.Context, user *model.User, settingsForm *form.SettingsForm) (tplParams, error) { + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + return args, err + } + + if settingsForm == nil { + args["form"] = form.SettingsForm{ + Username: user.Username, + Theme: user.Theme, + Language: user.Language, + Timezone: user.Timezone, + } + } else { + args["form"] = settingsForm + } + + args["menu"] = "settings" + args["themes"] = model.GetThemes() + args["languages"] = locale.GetAvailableLanguages() + args["timezones"], err = c.store.GetTimezones() + if err != nil { + return args, err + } + + return args, nil +} diff --git a/server/ui/controller/static.go b/server/ui/controller/static.go new file mode 100644 index 00000000..7b6a1def --- /dev/null +++ b/server/ui/controller/static.go @@ -0,0 +1,41 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "encoding/base64" + "github.com/miniflux/miniflux2/server/core" + "github.com/miniflux/miniflux2/server/static" + "log" + "time" +) + +func (c *Controller) Stylesheet(ctx *core.Context, request *core.Request, response *core.Response) { + stylesheet := request.GetStringParam("name", "white") + body := static.Stylesheets["common"] + etag := static.StylesheetsChecksums["common"] + + if theme, found := static.Stylesheets[stylesheet]; found { + body += theme + etag += static.StylesheetsChecksums[stylesheet] + } + + response.Cache("text/css", etag, []byte(body), 48*time.Hour) +} + +func (c *Controller) Javascript(ctx *core.Context, request *core.Request, response *core.Response) { + response.Cache("text/javascript", static.JavascriptChecksums["app"], []byte(static.Javascript["app"]), 48*time.Hour) +} + +func (c *Controller) Favicon(ctx *core.Context, request *core.Request, response *core.Response) { + blob, err := base64.StdEncoding.DecodeString(static.Binaries["favicon.ico"]) + if err != nil { + log.Println(err) + response.Html().NotFound() + return + } + + response.Cache("image/x-icon", static.BinariesChecksums["favicon.ico"], blob, 48*time.Hour) +} diff --git a/server/ui/controller/subscription.go b/server/ui/controller/subscription.go new file mode 100644 index 00000000..b1557696 --- /dev/null +++ b/server/ui/controller/subscription.go @@ -0,0 +1,127 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "github.com/miniflux/miniflux2/model" + "github.com/miniflux/miniflux2/reader/subscription" + "github.com/miniflux/miniflux2/server/core" + "github.com/miniflux/miniflux2/server/ui/form" + "log" +) + +func (c *Controller) AddSubscription(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + args, err := c.getSubscriptionFormTemplateArgs(ctx, user) + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("add_subscription", args) +} + +func (c *Controller) SubmitSubscription(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + args, err := c.getSubscriptionFormTemplateArgs(ctx, user) + if err != nil { + response.Html().ServerError(err) + return + } + + subscriptionForm := form.NewSubscriptionForm(request.GetRequest()) + if err := subscriptionForm.Validate(); err != nil { + response.Html().Render("add_subscription", args.Merge(tplParams{ + "form": subscriptionForm, + "errorMessage": err.Error(), + })) + return + } + + subscriptions, err := subscription.FindSubscriptions(subscriptionForm.URL) + if err != nil { + log.Println(err) + response.Html().Render("add_subscription", args.Merge(tplParams{ + "form": subscriptionForm, + "errorMessage": err, + })) + return + } + + log.Println("[UI:SubmitSubscription]", subscriptions) + + n := len(subscriptions) + switch { + case n == 0: + response.Html().Render("add_subscription", args.Merge(tplParams{ + "form": subscriptionForm, + "errorMessage": "Unable to find any subscription.", + })) + case n == 1: + feed, err := c.feedHandler.CreateFeed(user.ID, subscriptionForm.CategoryID, subscriptions[0].URL) + if err != nil { + response.Html().Render("add_subscription", args.Merge(tplParams{ + "form": subscriptionForm, + "errorMessage": err, + })) + return + } + + response.Redirect(ctx.GetRoute("feedEntries", "feedID", feed.ID)) + case n > 1: + response.Html().Render("choose_subscription", args.Merge(tplParams{ + "categoryID": subscriptionForm.CategoryID, + "subscriptions": subscriptions, + })) + } +} + +func (c *Controller) ChooseSubscription(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + args, err := c.getSubscriptionFormTemplateArgs(ctx, user) + if err != nil { + response.Html().ServerError(err) + return + } + + subscriptionForm := form.NewSubscriptionForm(request.GetRequest()) + if err := subscriptionForm.Validate(); err != nil { + response.Html().Render("add_subscription", args.Merge(tplParams{ + "form": subscriptionForm, + "errorMessage": err.Error(), + })) + return + } + + feed, err := c.feedHandler.CreateFeed(user.ID, subscriptionForm.CategoryID, subscriptionForm.URL) + if err != nil { + response.Html().Render("add_subscription", args.Merge(tplParams{ + "form": subscriptionForm, + "errorMessage": err, + })) + return + } + + response.Redirect(ctx.GetRoute("feedEntries", "feedID", feed.ID)) +} + +func (c *Controller) getSubscriptionFormTemplateArgs(ctx *core.Context, user *model.User) (tplParams, error) { + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + return nil, err + } + + categories, err := c.store.GetCategories(user.ID) + if err != nil { + return nil, err + } + + args["categories"] = categories + args["menu"] = "feeds" + return args, nil +} diff --git a/server/ui/controller/unread.go b/server/ui/controller/unread.go new file mode 100644 index 00000000..63d7db02 --- /dev/null +++ b/server/ui/controller/unread.go @@ -0,0 +1,43 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "github.com/miniflux/miniflux2/model" + "github.com/miniflux/miniflux2/server/core" +) + +func (c *Controller) ShowUnreadPage(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + offset := request.GetQueryIntegerParam("offset", 0) + + builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone) + builder.WithStatus(model.EntryStatusUnread) + builder.WithOrder(model.DefaultSortingOrder) + builder.WithDirection(model.DefaultSortingDirection) + builder.WithOffset(offset) + builder.WithLimit(NbItemsPerPage) + + entries, err := builder.GetEntries() + if err != nil { + response.Html().ServerError(err) + return + } + + countUnread, err := builder.CountEntries() + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("unread", tplParams{ + "user": user, + "countUnread": countUnread, + "entries": entries, + "pagination": c.getPagination(ctx.GetRoute("unread"), countUnread, offset), + "menu": "unread", + "csrf": ctx.GetCsrfToken(), + }) +} diff --git a/server/ui/controller/user.go b/server/ui/controller/user.go new file mode 100644 index 00000000..c69b0f8d --- /dev/null +++ b/server/ui/controller/user.go @@ -0,0 +1,231 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package controller + +import ( + "errors" + "github.com/miniflux/miniflux2/model" + "github.com/miniflux/miniflux2/server/core" + "github.com/miniflux/miniflux2/server/ui/form" + "log" +) + +func (c *Controller) ShowUsers(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + if !user.IsAdmin { + response.Html().Forbidden() + return + } + + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + users, err := c.store.GetUsers() + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("users", args.Merge(tplParams{ + "users": users, + "menu": "settings", + })) +} + +func (c *Controller) CreateUser(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + if !user.IsAdmin { + response.Html().Forbidden() + return + } + + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + response.Html().Render("create_user", args.Merge(tplParams{ + "menu": "settings", + "form": &form.UserForm{}, + })) +} + +func (c *Controller) SaveUser(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + if !user.IsAdmin { + response.Html().Forbidden() + return + } + + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + userForm := form.NewUserForm(request.GetRequest()) + if err := userForm.ValidateCreation(); err != nil { + response.Html().Render("create_user", args.Merge(tplParams{ + "menu": "settings", + "form": userForm, + "errorMessage": err.Error(), + })) + return + } + + if c.store.UserExists(userForm.Username) { + response.Html().Render("create_user", args.Merge(tplParams{ + "menu": "settings", + "form": userForm, + "errorMessage": "This user already exists.", + })) + return + } + + newUser := userForm.ToUser() + if err := c.store.CreateUser(newUser); err != nil { + log.Println(err) + response.Html().Render("edit_user", args.Merge(tplParams{ + "menu": "settings", + "form": userForm, + "errorMessage": "Unable to create this user.", + })) + return + } + + response.Redirect(ctx.GetRoute("users")) +} + +func (c *Controller) EditUser(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + if !user.IsAdmin { + response.Html().Forbidden() + return + } + + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + selectedUser, err := c.getUserFromURL(ctx, request, response) + if err != nil { + return + } + + response.Html().Render("edit_user", args.Merge(tplParams{ + "menu": "settings", + "selected_user": selectedUser, + "form": &form.UserForm{ + Username: selectedUser.Username, + IsAdmin: selectedUser.IsAdmin, + }, + })) +} + +func (c *Controller) UpdateUser(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + + if !user.IsAdmin { + response.Html().Forbidden() + return + } + + args, err := c.getCommonTemplateArgs(ctx) + if err != nil { + response.Html().ServerError(err) + return + } + + selectedUser, err := c.getUserFromURL(ctx, request, response) + if err != nil { + return + } + + userForm := form.NewUserForm(request.GetRequest()) + if err := userForm.ValidateModification(); err != nil { + response.Html().Render("edit_user", args.Merge(tplParams{ + "menu": "settings", + "selected_user": selectedUser, + "form": userForm, + "errorMessage": err.Error(), + })) + return + } + + if c.store.AnotherUserExists(selectedUser.ID, userForm.Username) { + response.Html().Render("edit_user", args.Merge(tplParams{ + "menu": "settings", + "selected_user": selectedUser, + "form": userForm, + "errorMessage": "This user already exists.", + })) + return + } + + userForm.Merge(selectedUser) + if err := c.store.UpdateUser(selectedUser); err != nil { + log.Println(err) + response.Html().Render("edit_user", args.Merge(tplParams{ + "menu": "settings", + "selected_user": selectedUser, + "form": userForm, + "errorMessage": "Unable to update this user.", + })) + return + } + + response.Redirect(ctx.GetRoute("users")) +} + +func (c *Controller) RemoveUser(ctx *core.Context, request *core.Request, response *core.Response) { + user := ctx.GetLoggedUser() + if !user.IsAdmin { + response.Html().Forbidden() + return + } + + selectedUser, err := c.getUserFromURL(ctx, request, response) + if err != nil { + return + } + + if err := c.store.RemoveUser(selectedUser.ID); err != nil { + response.Html().ServerError(err) + return + } + + response.Redirect(ctx.GetRoute("users")) +} + +func (c *Controller) getUserFromURL(ctx *core.Context, request *core.Request, response *core.Response) (*model.User, error) { + userID, err := request.GetIntegerParam("userID") + if err != nil { + response.Html().BadRequest(err) + return nil, err + } + + user, err := c.store.GetUserById(userID) + if err != nil { + response.Html().ServerError(err) + return nil, err + } + + if user == nil { + response.Html().NotFound() + return nil, errors.New("User not found") + } + + return user, nil +} |