Allow data syncing and deletion
This commit is contained in:
parent
e0bc0d2e81
commit
2faaa2e18b
|
@ -1,7 +1,7 @@
|
||||||
const {redirect} = require("pinski/plugins")
|
const {redirect} = require("pinski/plugins")
|
||||||
const db = require("../utils/db")
|
const db = require("../utils/db")
|
||||||
const constants = require("../utils/constants")
|
const constants = require("../utils/constants")
|
||||||
const {getUser} = require("../utils/getuser")
|
const {getUser, setToken} = require("../utils/getuser")
|
||||||
const validate = require("../utils/validate")
|
const validate = require("../utils/validate")
|
||||||
const V = validate.V
|
const V = validate.V
|
||||||
const {fetchChannel} = require("../utils/youtube")
|
const {fetchChannel} = require("../utils/youtube")
|
||||||
|
@ -54,5 +54,45 @@ module.exports = [
|
||||||
})
|
})
|
||||||
.go()
|
.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}) => {
|
route: "/settings", methods: ["GET"], code: async ({req}) => {
|
||||||
const user = getUser(req)
|
const user = getUser(req)
|
||||||
const settings = user.getSettings()
|
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)
|
videos = db.prepare(`SELECT * FROM Videos WHERE authorId IN (${template}) ORDER BY published DESC LIMIT 60`).all(subscriptions)
|
||||||
.map(video => {
|
.map(video => {
|
||||||
video.publishedText = timeToPastText(video.published * 1000)
|
video.publishedText = timeToPastText(video.published * 1000)
|
||||||
console.log(watchedVideos, video.videoId)
|
|
||||||
video.watched = watchedVideos.includes(video.videoId)
|
video.watched = watchedVideos.includes(video.videoId)
|
||||||
return video
|
return video
|
||||||
})
|
})
|
||||||
|
|
|
@ -49,3 +49,32 @@ block content
|
||||||
|
|
||||||
.save-settings
|
.save-settings
|
||||||
button.border-look Save
|
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
|
- const notLoaded = channels.length - refreshed.count
|
||||||
if notLoaded
|
if notLoaded
|
||||||
div #{notLoaded} subscriptions have not been refreshed at all
|
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
|
if settings.save_history
|
||||||
input(type="checkbox" id="watched-videos-display")
|
input(type="checkbox" id="watched-videos-display")
|
||||||
|
|
|
@ -79,3 +79,25 @@ fieldset
|
||||||
&.checkbox:not(:disabled) + .pill
|
&.checkbox:not(:disabled) + .pill
|
||||||
@include acts-like-button
|
@include acts-like-button
|
||||||
cursor: pointer
|
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
|
.settings-page
|
||||||
padding: 40px 20px 20px
|
padding: 40px 20px 20px
|
||||||
max-width: 600px
|
max-width: 600px
|
||||||
|
@ -9,3 +12,18 @@
|
||||||
.border-look
|
.border-look
|
||||||
font-size: 22px
|
font-size: 22px
|
||||||
padding: 7px 16px 8px
|
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 "colors.sass" as c
|
||||||
@use "video-list-item.sass" as *
|
@use "video-list-item.sass" as *
|
||||||
|
@use "forms.sass" as forms
|
||||||
|
|
||||||
.subscriptions-page
|
.subscriptions-page
|
||||||
padding: 40px 20px 20px
|
padding: 40px 20px 20px
|
||||||
|
@ -34,27 +35,7 @@
|
||||||
font-size: 22px
|
font-size: 22px
|
||||||
color: c.$fg-main
|
color: c.$fg-main
|
||||||
|
|
||||||
|
@include forms.checkbox-hider("watched-videos-display")
|
||||||
#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
|
|
||||||
|
|
||||||
#watched-videos-display:checked ~ .video-list-item--watched
|
#watched-videos-display:checked ~ .video-list-item--watched
|
||||||
display: none
|
display: none
|
||||||
|
|
|
@ -9,10 +9,7 @@ function getToken(req, responseHeaders) {
|
||||||
let token = cookie.token
|
let token = cookie.token
|
||||||
if (!token) {
|
if (!token) {
|
||||||
if (responseHeaders) { // we should create a token
|
if (responseHeaders) { // we should create a token
|
||||||
const setCookie = responseHeaders["set-cookie"] || []
|
setToken(responseHeaders)
|
||||||
token = crypto.randomBytes(18).toString("base64").replace(/\W/g, "_")
|
|
||||||
setCookie.push(`token=${token}; Path=/; Max-Age=2147483648; HttpOnly; SameSite=Lax`)
|
|
||||||
responseHeaders["set-cookie"] = setCookie
|
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -21,6 +18,14 @@ function getToken(req, responseHeaders) {
|
||||||
return token
|
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 {
|
class User {
|
||||||
constructor(token) {
|
constructor(token) {
|
||||||
this.token = token
|
this.token = token
|
||||||
|
@ -107,6 +112,7 @@ cleanCSRF()
|
||||||
setInterval(cleanCSRF, constants.caching.csrf_time).unref()
|
setInterval(cleanCSRF, constants.caching.csrf_time).unref()
|
||||||
|
|
||||||
module.exports.getToken = getToken
|
module.exports.getToken = getToken
|
||||||
|
module.exports.setToken = setToken
|
||||||
module.exports.generateCSRF = generateCSRF
|
module.exports.generateCSRF = generateCSRF
|
||||||
module.exports.checkCSRF = checkCSRF
|
module.exports.checkCSRF = checkCSRF
|
||||||
module.exports.getUser = getUser
|
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.V = V
|
||||||
module.exports.presetLoad = presetLoad
|
module.exports.presetLoad = presetLoad
|
||||||
module.exports.presetURLParamsBody = presetURLParamsBody
|
module.exports.presetURLParamsBody = presetURLParamsBody
|
||||||
|
module.exports.presetEnsureParams = presetEnsureParams
|
||||||
|
|
Loading…
Reference in New Issue