1
0
mirror of https://git.sr.ht/~cadence/cloudtube synced 2024-12-22 13:07:00 +00:00

Allow data syncing and deletion

This commit is contained in:
Cadence Ember 2020-12-29 16:21:48 +13:00
parent e0bc0d2e81
commit 2faaa2e18b
No known key found for this signature in database
GPG Key ID: BC1C2C61CF521B17
10 changed files with 138 additions and 28 deletions

View File

@ -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"
}
}
}
}
]

View File

@ -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})
}
},
{

View File

@ -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
})

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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