Allow data syncing and deletion
This commit is contained in:
parent
e0bc0d2e81
commit
2faaa2e18b
|
@ -1,7 +1,7 @@
|
|||
const {redirect} = require("pinski/plugins")
|
||||
const db = require("../utils/db")
|
||||
const constants = require("../utils/constants")
|
||||
const {getUser} = require("../utils/getuser")
|
||||
const {getUser, setToken} = require("../utils/getuser")
|
||||
const validate = require("../utils/validate")
|
||||
const V = validate.V
|
||||
const {fetchChannel} = require("../utils/youtube")
|
||||
|
@ -54,5 +54,45 @@ module.exports = [
|
|||
})
|
||||
.go()
|
||||
}
|
||||
},
|
||||
{
|
||||
route: `/formapi/erase`, methods: ["POST"], upload: true, code: async ({req, fill, body}) => {
|
||||
return new V()
|
||||
.with(validate.presetLoad({body}))
|
||||
.with(validate.presetURLParamsBody())
|
||||
.with(validate.presetEnsureParams(["token"]))
|
||||
.last(async state => {
|
||||
const {params} = state
|
||||
const token = params.get("token")
|
||||
;["Subscriptions", "Settings", "SeenTokens", "WatchedVideos"].forEach(table => {
|
||||
db.prepare(`DELETE FROM ${table} WHERE token = ?`).run(token)
|
||||
})
|
||||
return {
|
||||
statusCode: 303,
|
||||
headers: {
|
||||
location: "/",
|
||||
"set-cookie": `token=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax`
|
||||
},
|
||||
content: {
|
||||
status: "ok"
|
||||
}
|
||||
}
|
||||
})
|
||||
.go()
|
||||
}
|
||||
},
|
||||
{
|
||||
route: "/formapi/importsession/(\\w+)", methods: ["GET"], code: async ({req, fill}) => {
|
||||
return {
|
||||
statusCode: 303,
|
||||
headers: setToken({
|
||||
location: "/subscriptions"
|
||||
}, fill[0]),
|
||||
contentType: "application/json",
|
||||
content: {
|
||||
status: "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -10,7 +10,7 @@ module.exports = [
|
|||
route: "/settings", methods: ["GET"], code: async ({req}) => {
|
||||
const user = getUser(req)
|
||||
const settings = user.getSettings()
|
||||
return render(200, "pug/settings.pug", {constants, settings})
|
||||
return render(200, "pug/settings.pug", {constants, user, settings})
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -31,7 +31,6 @@ module.exports = [
|
|||
videos = db.prepare(`SELECT * FROM Videos WHERE authorId IN (${template}) ORDER BY published DESC LIMIT 60`).all(subscriptions)
|
||||
.map(video => {
|
||||
video.publishedText = timeToPastText(video.published * 1000)
|
||||
console.log(watchedVideos, video.videoId)
|
||||
video.watched = watchedVideos.includes(video.videoId)
|
||||
return video
|
||||
})
|
||||
|
|
|
@ -49,3 +49,32 @@ block content
|
|||
|
||||
.save-settings
|
||||
button.border-look Save
|
||||
|
||||
details.data-management
|
||||
summary Sync data
|
||||
p Open this link elsewhere to import your current CloudTube session there.
|
||||
p.
|
||||
If you clear your cookies often, you can bookmark this link and open it
|
||||
to restore your data, or if you have multiple devices, you can send this
|
||||
link to them to import your session and automatically keep everything
|
||||
in sync.
|
||||
- let url = `/formapi/importsession/${user.token}`
|
||||
a(href=url)= url
|
||||
|
||||
details.data-management.delete-details
|
||||
summary Delete data
|
||||
p Press this button to erase all your data from CloudTube.
|
||||
p.
|
||||
Just the current session will be removed. If you lost access to a
|
||||
previous session, you cannot touch it.
|
||||
p.
|
||||
You will lose your subscriptions, watch history, settings, and anything
|
||||
else you stored on the server. The server will keep no record that they
|
||||
ever existed.
|
||||
p Deletion is instant and #[em cannot be undone.]
|
||||
input(type="checkbox" id="delete-confirm")
|
||||
.delete-confirm-container
|
||||
label(for="delete-confirm").delete-confirm-label I understand the consequences
|
||||
form(method="post" action="/formapi/erase")
|
||||
input(type="hidden" name="token" value=user.token)
|
||||
button.border-look#delete-button Permanently erase my data
|
||||
|
|
|
@ -26,6 +26,7 @@ block content
|
|||
- const notLoaded = channels.length - refreshed.count
|
||||
if notLoaded
|
||||
div #{notLoaded} subscriptions have not been refreshed at all
|
||||
div Your subscriptions will be regularly refreshed in the background so long as you log in frequently.
|
||||
|
||||
if settings.save_history
|
||||
input(type="checkbox" id="watched-videos-display")
|
||||
|
|
|
@ -79,3 +79,25 @@ fieldset
|
|||
&.checkbox:not(:disabled) + .pill
|
||||
@include acts-like-button
|
||||
cursor: pointer
|
||||
|
||||
@mixin checkbox-hider($base)
|
||||
##{$base}
|
||||
position: relative
|
||||
left: 10px
|
||||
display: block
|
||||
z-index: 1
|
||||
height: 42px
|
||||
margin: 0
|
||||
|
||||
.#{$base}-container
|
||||
position: relative
|
||||
display: grid // why does the default not work???
|
||||
top: -42px
|
||||
background: c.$bg-accent-x
|
||||
line-height: 1
|
||||
border-radius: 8px
|
||||
margin-bottom: -18px
|
||||
|
||||
.#{$base}-label
|
||||
padding: 12px 0px 12px 32px
|
||||
cursor: pointer
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
@use "forms.sass" as forms
|
||||
@use "colors.sass" as c
|
||||
|
||||
.settings-page
|
||||
padding: 40px 20px 20px
|
||||
max-width: 600px
|
||||
|
@ -9,3 +12,18 @@
|
|||
.border-look
|
||||
font-size: 22px
|
||||
padding: 7px 16px 8px
|
||||
|
||||
.data-management
|
||||
margin-top: 24px
|
||||
|
||||
.delete-confirm-container
|
||||
background: c.$bg-darker
|
||||
margin-bottom: -36px
|
||||
|
||||
@include forms.checkbox-hider("delete-confirm")
|
||||
|
||||
#delete-confirm:not(:checked) ~ * #delete-button
|
||||
visibility: hidden
|
||||
|
||||
.delete-details[open]
|
||||
padding-bottom: 40px
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@use "colors.sass" as c
|
||||
@use "video-list-item.sass" as *
|
||||
@use "forms.sass" as forms
|
||||
|
||||
.subscriptions-page
|
||||
padding: 40px 20px 20px
|
||||
|
@ -34,27 +35,7 @@
|
|||
font-size: 22px
|
||||
color: c.$fg-main
|
||||
|
||||
|
||||
#watched-videos-display
|
||||
position: relative
|
||||
left: 10px
|
||||
display: block
|
||||
z-index: 1
|
||||
height: 42px
|
||||
margin: 0
|
||||
|
||||
.watched-videos-display-container
|
||||
position: relative
|
||||
display: grid // why does the default not work???
|
||||
top: -42px
|
||||
background: c.$bg-accent-x
|
||||
line-height: 1
|
||||
border-radius: 8px
|
||||
margin-bottom: -18px
|
||||
|
||||
.watched-videos-display-label
|
||||
padding: 12px 0px 12px 32px
|
||||
cursor: pointer
|
||||
@include forms.checkbox-hider("watched-videos-display")
|
||||
|
||||
#watched-videos-display:checked ~ .video-list-item--watched
|
||||
display: none
|
||||
|
|
|
@ -9,10 +9,7 @@ function getToken(req, responseHeaders) {
|
|||
let token = cookie.token
|
||||
if (!token) {
|
||||
if (responseHeaders) { // we should create a token
|
||||
const setCookie = responseHeaders["set-cookie"] || []
|
||||
token = crypto.randomBytes(18).toString("base64").replace(/\W/g, "_")
|
||||
setCookie.push(`token=${token}; Path=/; Max-Age=2147483648; HttpOnly; SameSite=Lax`)
|
||||
responseHeaders["set-cookie"] = setCookie
|
||||
setToken(responseHeaders)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
@ -21,6 +18,14 @@ function getToken(req, responseHeaders) {
|
|||
return token
|
||||
}
|
||||
|
||||
function setToken(responseHeaders, token) {
|
||||
const setCookie = responseHeaders["set-cookie"] || []
|
||||
if (!token) token = crypto.randomBytes(18).toString("base64").replace(/\W/g, "_")
|
||||
setCookie.push(`token=${token}; Path=/; Max-Age=2147483648; HttpOnly; SameSite=Lax`)
|
||||
responseHeaders["set-cookie"] = setCookie
|
||||
return responseHeaders
|
||||
}
|
||||
|
||||
class User {
|
||||
constructor(token) {
|
||||
this.token = token
|
||||
|
@ -107,6 +112,7 @@ cleanCSRF()
|
|||
setInterval(cleanCSRF, constants.caching.csrf_time).unref()
|
||||
|
||||
module.exports.getToken = getToken
|
||||
module.exports.setToken = setToken
|
||||
module.exports.generateCSRF = generateCSRF
|
||||
module.exports.checkCSRF = checkCSRF
|
||||
module.exports.getUser = getUser
|
||||
|
|
|
@ -78,6 +78,20 @@ function presetURLParamsBody() {
|
|||
]
|
||||
}
|
||||
|
||||
function presetEnsureParams(list) {
|
||||
return [
|
||||
state => {
|
||||
return list.every(name => state.params.has(name))
|
||||
},
|
||||
() => ({
|
||||
statusCode: 400,
|
||||
contentType: "application/json",
|
||||
content: `Some required body parameters were missing. Required parameters: ${list.join(", ")}`
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
module.exports.V = V
|
||||
module.exports.presetLoad = presetLoad
|
||||
module.exports.presetURLParamsBody = presetURLParamsBody
|
||||
module.exports.presetEnsureParams = presetEnsureParams
|
||||
|
|
Loading…
Reference in New Issue