1
0

User Data Export and import (#967)

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/api/pulls/967
Co-authored-by: konrad <k@knt.li>
Co-committed-by: konrad <k@knt.li>
This commit is contained in:
konrad
2021-09-04 19:26:31 +00:00
parent fc51a3e76f
commit 90146aea5b
46 changed files with 2395 additions and 582 deletions

View File

@ -19,6 +19,8 @@ package v1
import (
"net/http"
vikunja_file "code.vikunja.io/api/pkg/modules/migration/vikunja-file"
microsofttodo "code.vikunja.io/api/pkg/modules/migration/microsoft-todo"
"code.vikunja.io/api/pkg/modules/migration/trello"
@ -91,6 +93,9 @@ func Info(c echo.Context) error {
CaldavEnabled: config.ServiceEnableCaldav.GetBool(),
EmailRemindersEnabled: config.ServiceEnableEmailReminders.GetBool(),
UserDeletionEnabled: config.ServiceEnableUserDeletion.GetBool(),
AvailableMigrators: []string{
(&vikunja_file.FileMigrator{}).Name(),
},
Legal: legalInfo{
ImprintURL: config.LegalImprintURL.GetString(),
PrivacyPolicyURL: config.LegalPrivacyURL.GetString(),

View File

@ -27,7 +27,7 @@ import (
"github.com/labstack/echo/v4"
)
type UserDeletionRequest struct {
type UserPasswordConfirmation struct {
Password string `json:"password" valid:"required"`
}
@ -41,13 +41,13 @@ type UserDeletionRequestConfirm struct {
// @tags user
// @Accept json
// @Produce json
// @Param credentials body v1.UserDeletionRequest true "The user password."
// @Param credentials body v1.UserPasswordConfirmation true "The user password."
// @Success 200 {object} models.Message
// @Failure 412 {object} web.HTTPError "Bad password provided."
// @Failure 500 {object} models.Message "Internal error"
// @Router /user/deletion/request [post]
func UserRequestDeletion(c echo.Context) error {
var deletionRequest UserDeletionRequest
var deletionRequest UserPasswordConfirmation
if err := c.Bind(&deletionRequest); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
}
@ -149,13 +149,13 @@ func UserConfirmDeletion(c echo.Context) error {
// @tags user
// @Accept json
// @Produce json
// @Param credentials body v1.UserDeletionRequest true "The user password to confirm."
// @Param credentials body v1.UserPasswordConfirmation true "The user password to confirm."
// @Success 200 {object} models.Message
// @Failure 412 {object} web.HTTPError "Bad password provided."
// @Failure 500 {object} models.Message "Internal error"
// @Router /user/deletion/cancel [post]
func UserCancelDeletion(c echo.Context) error {
var deletionRequest UserDeletionRequest
var deletionRequest UserPasswordConfirmation
if err := c.Bind(&deletionRequest); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
}

View File

@ -0,0 +1,136 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package v1
import (
"net/http"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web/handler"
"github.com/labstack/echo/v4"
"xorm.io/xorm"
)
func checkExportRequest(c echo.Context) (s *xorm.Session, u *user.User, err error) {
var pass UserPasswordConfirmation
if err := c.Bind(&pass); err != nil {
return nil, nil, echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
}
err = c.Validate(pass)
if err != nil {
return nil, nil, echo.NewHTTPError(http.StatusBadRequest, err)
}
s = db.NewSession()
defer s.Close()
err = s.Begin()
if err != nil {
return nil, nil, handler.HandleHTTPError(err, c)
}
u, err = user.GetCurrentUserFromDB(s, c)
if err != nil {
_ = s.Rollback()
return nil, nil, handler.HandleHTTPError(err, c)
}
err = user.CheckUserPassword(u, pass.Password)
if err != nil {
_ = s.Rollback()
return nil, nil, handler.HandleHTTPError(err, c)
}
return
}
// RequestUserDataExport is the handler to request a user data export
// @Summary Request a user data export.
// @tags user
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param password body v1.UserPasswordConfirmation true "User password to confirm the data export request."
// @Success 200 {object} models.Message
// @Failure 400 {object} web.HTTPError "Something's invalid."
// @Failure 500 {object} models.Message "Internal server error."
// @Router /user/export/request [post]
func RequestUserDataExport(c echo.Context) error {
s, u, err := checkExportRequest(c)
if err != nil {
return err
}
err = events.Dispatch(&models.UserDataExportRequestedEvent{
User: u,
})
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
}
err = s.Commit()
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
}
return c.JSON(http.StatusOK, models.Message{Message: "Successfully requested data export. We will send you an email when it's ready."})
}
// DownloadUserDataExport is the handler to download a created user data export
// @Summary Download a user data export.
// @tags user
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param password body v1.UserPasswordConfirmation true "User password to confirm the download."
// @Success 200 {object} models.Message
// @Failure 400 {object} web.HTTPError "Something's invalid."
// @Failure 500 {object} models.Message "Internal server error."
// @Router /user/export/download [post]
func DownloadUserDataExport(c echo.Context) error {
s, u, err := checkExportRequest(c)
if err != nil {
return err
}
err = s.Commit()
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
}
// Download
exportFile := &files.File{ID: u.ExportFileID}
err = exportFile.LoadFileMetaByID()
if err != nil {
return handler.HandleHTTPError(err, c)
}
err = exportFile.LoadFileByID()
if err != nil {
return handler.HandleHTTPError(err, c)
}
http.ServeContent(c.Response(), c.Request(), exportFile.Name, exportFile.Created, exportFile.File)
return nil
}

View File

@ -56,7 +56,7 @@ func ListHandler(c echo.Context) error {
}
storage := &VikunjaCaldavListStorage{
list: &models.List{ID: listID},
list: &models.ListWithTasksAndBuckets{List: models.List{ID: listID}},
user: u,
}
@ -102,7 +102,7 @@ func TaskHandler(c echo.Context) error {
taskUID := strings.TrimSuffix(c.Param("task"), ".ics")
storage := &VikunjaCaldavListStorage{
list: &models.List{ID: listID},
list: &models.ListWithTasksAndBuckets{List: models.List{ID: listID}},
task: &models.Task{UID: taskUID},
user: u,
}

View File

@ -39,7 +39,7 @@ const ListBasePath = DavBasePath + `lists`
// VikunjaCaldavListStorage represents a list storage
type VikunjaCaldavListStorage struct {
// Used when handling a list
list *models.List
list *models.ListWithTasksAndBuckets
// Used when handling a single task, like updating
task *models.Task
// The current user
@ -109,7 +109,9 @@ func (vcls *VikunjaCaldavListStorage) GetResources(rpath string, withChildren bo
var resources []data.Resource
for _, l := range lists {
rr := VikunjaListResourceAdapter{
list: l,
list: &models.ListWithTasksAndBuckets{
List: *l,
},
isCollection: true,
}
r := data.NewResource(ListBasePath+"/"+strconv.FormatInt(l.ID, 10), &rr)
@ -172,10 +174,10 @@ func (vcls *VikunjaCaldavListStorage) GetResourcesByFilters(rpath string, filter
for _, t := range vcls.list.Tasks {
rr := VikunjaListResourceAdapter{
list: vcls.list,
task: t,
task: &t.Task,
isCollection: false,
}
r := data.NewResource(getTaskURL(t), &rr)
r := data.NewResource(getTaskURL(&t.Task), &rr)
r.Name = t.Title
resources = append(resources, r)
}
@ -368,8 +370,8 @@ func (vcls *VikunjaCaldavListStorage) DeleteResource(rpath string) error {
// VikunjaListResourceAdapter holds the actual resource
type VikunjaListResourceAdapter struct {
list *models.List
listTasks []*models.Task
list *models.ListWithTasksAndBuckets
listTasks []*models.TaskWithComments
task *models.Task
isPrincipal bool
@ -415,7 +417,7 @@ func (vlra *VikunjaListResourceAdapter) GetContent() string {
}
if vlra.task != nil {
list := models.List{Tasks: []*models.Task{vlra.task}}
list := models.ListWithTasksAndBuckets{Tasks: []*models.TaskWithComments{{Task: *vlra.task}}}
return caldav.GetCaldavTodosForTasks(&list, list.Tasks)
}
@ -479,8 +481,10 @@ func (vcls *VikunjaCaldavListStorage) getListRessource(isCollection bool) (rr Vi
panic("Tasks returned from TaskCollection.ReadAll are not []*models.Task!")
}
listTasks = tasks
vcls.list.Tasks = tasks
for _, t := range tasks {
listTasks = append(listTasks, &models.TaskWithComments{Task: *t})
}
vcls.list.Tasks = listTasks
}
if err := s.Commit(); err != nil {

View File

@ -52,6 +52,8 @@ import (
"strings"
"time"
vikunja_file "code.vikunja.io/api/pkg/modules/migration/vikunja-file"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
@ -303,6 +305,8 @@ func registerAPIRoutes(a *echo.Group) {
u.POST("/settings/avatar", apiv1.ChangeUserAvatarProvider)
u.PUT("/settings/avatar/upload", apiv1.UploadAvatar)
u.POST("/settings/general", apiv1.UpdateGeneralUserSettings)
u.POST("/export/request", apiv1.RequestUserDataExport)
u.POST("/export/download", apiv1.DownloadUserDataExport)
if config.ServiceEnableTotp.GetBool() {
u.GET("/settings/totp", apiv1.UserTOTP)
@ -563,7 +567,35 @@ func registerAPIRoutes(a *echo.Group) {
// Migrations
m := a.Group("/migration")
registerMigrations(m)
// List Backgrounds
if config.BackgroundsEnabled.GetBool() {
a.GET("/lists/:list/background", backgroundHandler.GetListBackground)
a.DELETE("/lists/:list/background", backgroundHandler.RemoveListBackground)
if config.BackgroundsUploadEnabled.GetBool() {
uploadBackgroundProvider := &backgroundHandler.BackgroundProvider{
Provider: func() background.Provider {
return &upload.Provider{}
},
}
a.PUT("/lists/:list/backgrounds/upload", uploadBackgroundProvider.UploadBackground)
}
if config.BackgroundsUnsplashEnabled.GetBool() {
unsplashBackgroundProvider := &backgroundHandler.BackgroundProvider{
Provider: func() background.Provider {
return &unsplash.Provider{}
},
}
a.GET("/backgrounds/unsplash/search", unsplashBackgroundProvider.SearchBackgrounds)
a.POST("/lists/:list/backgrounds/unsplash", unsplashBackgroundProvider.SetBackground)
a.GET("/backgrounds/unsplash/images/:image/thumb", unsplash.ProxyUnsplashThumb)
a.GET("/backgrounds/unsplash/images/:image", unsplash.ProxyUnsplashImage)
}
}
}
func registerMigrations(m *echo.Group) {
// Wunderlist
if config.MigrationWunderlistEnable.GetBool() {
wunderlistMigrationHandler := &migrationHandler.MigrationWeb{
@ -604,30 +636,12 @@ func registerAPIRoutes(a *echo.Group) {
microsoftTodoMigrationHandler.RegisterRoutes(m)
}
// List Backgrounds
if config.BackgroundsEnabled.GetBool() {
a.GET("/lists/:list/background", backgroundHandler.GetListBackground)
a.DELETE("/lists/:list/background", backgroundHandler.RemoveListBackground)
if config.BackgroundsUploadEnabled.GetBool() {
uploadBackgroundProvider := &backgroundHandler.BackgroundProvider{
Provider: func() background.Provider {
return &upload.Provider{}
},
}
a.PUT("/lists/:list/backgrounds/upload", uploadBackgroundProvider.UploadBackground)
}
if config.BackgroundsUnsplashEnabled.GetBool() {
unsplashBackgroundProvider := &backgroundHandler.BackgroundProvider{
Provider: func() background.Provider {
return &unsplash.Provider{}
},
}
a.GET("/backgrounds/unsplash/search", unsplashBackgroundProvider.SearchBackgrounds)
a.POST("/lists/:list/backgrounds/unsplash", unsplashBackgroundProvider.SetBackground)
a.GET("/backgrounds/unsplash/images/:image/thumb", unsplash.ProxyUnsplashThumb)
a.GET("/backgrounds/unsplash/images/:image", unsplash.ProxyUnsplashImage)
}
vikunjaFileMigrationHandler := &migrationHandler.FileMigratorWeb{
MigrationStruct: func() migration.FileMigrator {
return &vikunja_file.FileMigrator{}
},
}
vikunjaFileMigrationHandler.RegisterRoutes(m)
}
func registerCalDavRoutes(c *echo.Group) {