forked from midou/invidious
Add authentication API
This commit is contained in:
parent
301871aec6
commit
2a6c81a89d
359
src/invidious.cr
359
src/invidious.cr
@ -197,16 +197,20 @@ before_all do |env|
|
|||||||
if env.request.cookies.has_key? "SID"
|
if env.request.cookies.has_key? "SID"
|
||||||
sid = env.request.cookies["SID"].value
|
sid = env.request.cookies["SID"].value
|
||||||
|
|
||||||
|
if sid.starts_with? "v1:"
|
||||||
|
raise "Cannot use token as SID"
|
||||||
|
end
|
||||||
|
|
||||||
# Invidious users only have SID
|
# Invidious users only have SID
|
||||||
if !env.request.cookies.has_key? "SSID"
|
if !env.request.cookies.has_key? "SSID"
|
||||||
if email = PG_DB.query_one?("SELECT email FROM session_ids WHERE id = $1", sid, as: String)
|
if email = PG_DB.query_one?("SELECT email FROM session_ids WHERE id = $1", sid, as: String)
|
||||||
user = PG_DB.query_one("SELECT * FROM users WHERE email = $1", email, as: User)
|
user = PG_DB.query_one("SELECT * FROM users WHERE email = $1", email, as: User)
|
||||||
token = create_response(sid, {"signout", "watch_ajax", "subscription_ajax"}, HMAC_KEY, PG_DB, 1.week)
|
csrf_token = generate_response(sid, {":signout", ":watch_ajax", ":subscription_ajax", ":token_ajax", ":authorize_token"}, HMAC_KEY, PG_DB, 1.week)
|
||||||
|
|
||||||
preferences = user.preferences
|
preferences = user.preferences
|
||||||
|
|
||||||
env.set "sid", sid
|
env.set "sid", sid
|
||||||
env.set "token", token
|
env.set "csrf_token", csrf_token
|
||||||
env.set "user", user
|
env.set "user", user
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@ -215,12 +219,12 @@ before_all do |env|
|
|||||||
|
|
||||||
begin
|
begin
|
||||||
user, sid = get_user(sid, headers, PG_DB, false)
|
user, sid = get_user(sid, headers, PG_DB, false)
|
||||||
token = create_response(sid, {"signout", "watch_ajax", "subscription_ajax"}, HMAC_KEY, PG_DB, 1.week)
|
csrf_token = generate_response(sid, {":signout", ":watch_ajax", ":subscription_ajax", ":token_ajax", ":authorize_token"}, HMAC_KEY, PG_DB, 1.week)
|
||||||
|
|
||||||
preferences = user.preferences
|
preferences = user.preferences
|
||||||
|
|
||||||
env.set "sid", sid
|
env.set "sid", sid
|
||||||
env.set "token", token
|
env.set "csrf_token", csrf_token
|
||||||
env.set "user", user
|
env.set "user", user
|
||||||
rescue ex
|
rescue ex
|
||||||
end
|
end
|
||||||
@ -1096,9 +1100,10 @@ post "/login" do |env|
|
|||||||
answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
|
answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
|
||||||
|
|
||||||
begin
|
begin
|
||||||
validate_response(tokens[0], answer, env.request.path, HMAC_KEY, PG_DB, locale)
|
validate_request(tokens[0], answer, env.request, HMAC_KEY, PG_DB, locale)
|
||||||
rescue ex
|
rescue ex
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
|
env.response.status_code = 400
|
||||||
next templated "error"
|
next templated "error"
|
||||||
end
|
end
|
||||||
when "text"
|
when "text"
|
||||||
@ -1109,7 +1114,7 @@ post "/login" do |env|
|
|||||||
error_message = translate(locale, "Invalid CAPTCHA")
|
error_message = translate(locale, "Invalid CAPTCHA")
|
||||||
tokens.each_with_index do |token, i|
|
tokens.each_with_index do |token, i|
|
||||||
begin
|
begin
|
||||||
validate_response(token, answer, env.request.path, HMAC_KEY, PG_DB, locale)
|
validate_request(token, answer, env.request, HMAC_KEY, PG_DB, locale)
|
||||||
found_valid_captcha = true
|
found_valid_captcha = true
|
||||||
rescue ex
|
rescue ex
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
@ -1189,12 +1194,13 @@ post "/signout" do |env|
|
|||||||
if user
|
if user
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
sid = sid.as(String)
|
sid = sid.as(String)
|
||||||
token = env.params.body["token"]?
|
token = env.params.body["csrf_token"]?
|
||||||
|
|
||||||
begin
|
begin
|
||||||
validate_response(token, sid, env.request.path, HMAC_KEY, PG_DB, locale)
|
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale)
|
||||||
rescue ex
|
rescue ex
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
|
env.response.status_code = 400
|
||||||
next templated "error"
|
next templated "error"
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1424,12 +1430,18 @@ post "/watch_ajax" do |env|
|
|||||||
redirect = redirect == "true"
|
redirect = redirect == "true"
|
||||||
|
|
||||||
if !user
|
if !user
|
||||||
next env.redirect referer
|
if redirect
|
||||||
|
next env.redirect referer
|
||||||
|
else
|
||||||
|
error_message = {"error" => "No such user"}.to_json
|
||||||
|
env.response.status_code = 403
|
||||||
|
next error_message
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
sid = sid.as(String)
|
sid = sid.as(String)
|
||||||
token = env.params.body["token"]?
|
token = env.params.body["csrf_token"]?
|
||||||
|
|
||||||
id = env.params.query["id"]?
|
id = env.params.query["id"]?
|
||||||
if !id
|
if !id
|
||||||
@ -1437,19 +1449,16 @@ post "/watch_ajax" do |env|
|
|||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
user = user.as(User)
|
|
||||||
sid = sid.as(String)
|
|
||||||
token = env.params.body["token"]?
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
validate_response(token, sid, env.request.path, HMAC_KEY, PG_DB, locale)
|
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale)
|
||||||
rescue ex
|
rescue ex
|
||||||
if redirect
|
if redirect
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
|
env.response.status_code = 400
|
||||||
next templated "error"
|
next templated "error"
|
||||||
else
|
else
|
||||||
error_message = {"error" => ex.message}.to_json
|
error_message = {"error" => ex.message}.to_json
|
||||||
env.response.status_code = 500
|
env.response.status_code = 400
|
||||||
next error_message
|
next error_message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -1494,8 +1503,14 @@ get "/modify_notifications" do |env|
|
|||||||
redirect ||= "false"
|
redirect ||= "false"
|
||||||
redirect = redirect == "true"
|
redirect = redirect == "true"
|
||||||
|
|
||||||
if !user && !sid
|
if !user
|
||||||
next env.redirect referer
|
if redirect
|
||||||
|
next env.redirect referer
|
||||||
|
else
|
||||||
|
error_message = {"error" => "No such user"}.to_json
|
||||||
|
env.response.status_code = 403
|
||||||
|
next error_message
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
@ -1566,22 +1581,29 @@ post "/subscription_ajax" do |env|
|
|||||||
redirect = redirect == "true"
|
redirect = redirect == "true"
|
||||||
|
|
||||||
if !user
|
if !user
|
||||||
next env.redirect referer
|
if redirect
|
||||||
|
next env.redirect referer
|
||||||
|
else
|
||||||
|
error_message = {"error" => "No such user"}.to_json
|
||||||
|
env.response.status_code = 403
|
||||||
|
next error_message
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
sid = sid.as(String)
|
sid = sid.as(String)
|
||||||
token = env.params.body["token"]?
|
token = env.params.body["csrf_token"]?
|
||||||
|
|
||||||
begin
|
begin
|
||||||
validate_response(token, sid, env.request.path, HMAC_KEY, PG_DB, locale)
|
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale)
|
||||||
rescue ex
|
rescue ex
|
||||||
if redirect
|
if redirect
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
|
env.response.status_code = 400
|
||||||
next templated "error"
|
next templated "error"
|
||||||
else
|
else
|
||||||
error_message = {"error" => ex.message}.to_json
|
error_message = {"error" => ex.message}.to_json
|
||||||
env.response.status_code = 500
|
env.response.status_code = 400
|
||||||
next error_message
|
next error_message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -1660,9 +1682,9 @@ get "/subscription_manager" do |env|
|
|||||||
|
|
||||||
user = env.get? "user"
|
user = env.get? "user"
|
||||||
sid = env.get? "sid"
|
sid = env.get? "sid"
|
||||||
referer = get_referer(env, "/subscription_manager")
|
referer = get_referer(env)
|
||||||
|
|
||||||
if !user && !sid
|
if !user
|
||||||
next env.redirect referer
|
next env.redirect referer
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1856,7 +1878,7 @@ get "/delete_account" do |env|
|
|||||||
if user
|
if user
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
sid = sid.as(String)
|
sid = sid.as(String)
|
||||||
token = create_response(sid, {"delete_account"}, HMAC_KEY, PG_DB)
|
csrf_token = generate_response(sid, {":delete_account"}, HMAC_KEY, PG_DB)
|
||||||
|
|
||||||
templated "delete_account"
|
templated "delete_account"
|
||||||
else
|
else
|
||||||
@ -1874,12 +1896,13 @@ post "/delete_account" do |env|
|
|||||||
if user
|
if user
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
sid = sid.as(String)
|
sid = sid.as(String)
|
||||||
token = env.params.body["token"]?
|
token = env.params.body["csrf_token"]?
|
||||||
|
|
||||||
begin
|
begin
|
||||||
validate_response(token, sid, env.request.path, HMAC_KEY, PG_DB, locale)
|
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale)
|
||||||
rescue ex
|
rescue ex
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
|
env.response.status_code = 400
|
||||||
next templated "error"
|
next templated "error"
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1907,7 +1930,7 @@ get "/clear_watch_history" do |env|
|
|||||||
if user
|
if user
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
sid = sid.as(String)
|
sid = sid.as(String)
|
||||||
token = create_response(sid, {"clear_watch_history"}, HMAC_KEY, PG_DB)
|
csrf_token = generate_response(sid, {":clear_watch_history"}, HMAC_KEY, PG_DB)
|
||||||
|
|
||||||
templated "clear_watch_history"
|
templated "clear_watch_history"
|
||||||
else
|
else
|
||||||
@ -1925,12 +1948,13 @@ post "/clear_watch_history" do |env|
|
|||||||
if user
|
if user
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
sid = sid.as(String)
|
sid = sid.as(String)
|
||||||
token = env.params.body["token"]?
|
token = env.params.body["csrf_token"]?
|
||||||
|
|
||||||
begin
|
begin
|
||||||
validate_response(token, sid, env.request.path, HMAC_KEY, PG_DB, locale)
|
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale)
|
||||||
rescue ex
|
rescue ex
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
|
env.response.status_code = 400
|
||||||
next templated "error"
|
next templated "error"
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1940,6 +1964,137 @@ post "/clear_watch_history" do |env|
|
|||||||
env.redirect referer
|
env.redirect referer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO?
|
||||||
|
# get "/authorize_token" do |env|
|
||||||
|
# ...
|
||||||
|
# end
|
||||||
|
|
||||||
|
post "/authorize_token" do |env|
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env)
|
||||||
|
|
||||||
|
if user
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
token = env.params.body["csrf_token"]?
|
||||||
|
|
||||||
|
begin
|
||||||
|
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale)
|
||||||
|
rescue ex
|
||||||
|
error_message = ex.message
|
||||||
|
env.response.status_code = 400
|
||||||
|
next templated "error"
|
||||||
|
end
|
||||||
|
|
||||||
|
scopes = env.params.body.select { |k, v| k.match(/^scopes\[\d+\]$/) }.map { |k, v| v }
|
||||||
|
callback_url = env.params.body["callbackUrl"]?
|
||||||
|
expire = env.params.body["expire"]?.try &.to_i?
|
||||||
|
|
||||||
|
access_token = generate_token(user.email, scopes, expire, HMAC_KEY, PG_DB)
|
||||||
|
|
||||||
|
if callback_url
|
||||||
|
access_token = URI.escape(access_token)
|
||||||
|
url = URI.parse(callback_url)
|
||||||
|
|
||||||
|
if url.query
|
||||||
|
query = HTTP::Params.parse(url.query.not_nil!)
|
||||||
|
else
|
||||||
|
query = HTTP::Params.new
|
||||||
|
end
|
||||||
|
|
||||||
|
query["token"] = access_token
|
||||||
|
url.query = query.to_s
|
||||||
|
|
||||||
|
env.redirect url.to_s
|
||||||
|
else
|
||||||
|
csrf_token = ""
|
||||||
|
env.set "access_token", access_token
|
||||||
|
templated "authorize_token"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/token_manager" do |env|
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env, "/subscription_manager")
|
||||||
|
|
||||||
|
if !user
|
||||||
|
next env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
|
||||||
|
tokens = PG_DB.query_all("SELECT id, issued FROM session_ids WHERE email = $1 ORDER BY issued DESC", user.email, as: {session: String, issued: Time})
|
||||||
|
|
||||||
|
templated "token_manager"
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/token_ajax" do |env|
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env)
|
||||||
|
|
||||||
|
redirect = env.params.query["redirect"]?
|
||||||
|
redirect ||= "true"
|
||||||
|
redirect = redirect == "true"
|
||||||
|
|
||||||
|
if !user
|
||||||
|
if redirect
|
||||||
|
next env.redirect referer
|
||||||
|
else
|
||||||
|
error_message = {"error" => "No such user"}.to_json
|
||||||
|
env.response.status_code = 403
|
||||||
|
next error_message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
token = env.params.body["csrf_token"]?
|
||||||
|
|
||||||
|
begin
|
||||||
|
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale)
|
||||||
|
rescue ex
|
||||||
|
if redirect
|
||||||
|
error_message = ex.message
|
||||||
|
next templated "error"
|
||||||
|
else
|
||||||
|
error_message = {"error" => ex.message}.to_json
|
||||||
|
env.response.status_code = 400
|
||||||
|
next error_message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if env.params.query["action_revoke_token"]?
|
||||||
|
action = "action_revoke_token"
|
||||||
|
else
|
||||||
|
next env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
session = env.params.query["session"]?
|
||||||
|
session ||= ""
|
||||||
|
|
||||||
|
case action
|
||||||
|
when .starts_with? "action_revoke_token"
|
||||||
|
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1 AND email = $2", session, user.email)
|
||||||
|
end
|
||||||
|
|
||||||
|
if redirect
|
||||||
|
env.redirect referer
|
||||||
|
else
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
"{}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Feeds
|
# Feeds
|
||||||
|
|
||||||
get "/feed/top" do |env|
|
get "/feed/top" do |env|
|
||||||
@ -4127,6 +4282,142 @@ get "/api/v1/mixes/:rdid" do |env|
|
|||||||
response
|
response
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
# get "/api/v1/auth/preferences" do |env|
|
||||||
|
# ...
|
||||||
|
# end
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
# post "/api/v1/auth/preferences" do |env|
|
||||||
|
# ...
|
||||||
|
# end
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
# get "/api/v1/auth/subscriptions" do |env|
|
||||||
|
# ...
|
||||||
|
# end
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
# post "/api/v1/auth/subscriptions/:ucid" do |env|
|
||||||
|
# ...
|
||||||
|
# end
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
# delete "/api/v1/auth/subscriptions/:ucid" do |env|
|
||||||
|
# ...
|
||||||
|
# end
|
||||||
|
|
||||||
|
get "/api/v1/auth/tokens" do |env|
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
scopes = env.get("scopes").as(Array(String))
|
||||||
|
|
||||||
|
tokens = PG_DB.query_all("SELECT id, issued FROM session_ids WHERE email = $1", user.email, as: {session: String, issued: Time})
|
||||||
|
|
||||||
|
# Only allow user sessions to view other user sessions
|
||||||
|
# if !scopes.includes? [":*"]
|
||||||
|
# tokens.select { |token| token[:session].starts_with? "v1:" }
|
||||||
|
# end
|
||||||
|
|
||||||
|
JSON.build do |json|
|
||||||
|
json.array do
|
||||||
|
tokens.each do |token|
|
||||||
|
json.object do
|
||||||
|
json.field "session", token[:session]
|
||||||
|
json.field "issued", token[:issued].to_unix
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/api/v1/auth/tokens/register" do |env|
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
case env.request.headers["Content-Type"]?
|
||||||
|
when "application/x-www-form-urlencoded"
|
||||||
|
scopes = env.params.body.select { |k, v| k.match(/^scopes\[\d+\]$/) }.map { |k, v| v }
|
||||||
|
callback_url = env.params.body["callbackUrl"]?
|
||||||
|
expire = env.params.body["expire"]?.try &.to_i?
|
||||||
|
when "application/json"
|
||||||
|
scopes = env.params.json["scopes"].as(Array).map { |v| v.as_s }
|
||||||
|
callback_url = env.params.json["callbackUrl"]?.try &.as(String)
|
||||||
|
expire = env.params.json["expire"]?.try &.as(Int64)
|
||||||
|
else
|
||||||
|
error_message = {"error" => "Invalid or missing header 'Content-Type'"}.to_json
|
||||||
|
env.response.status_code = 400
|
||||||
|
next error_message
|
||||||
|
end
|
||||||
|
|
||||||
|
if callback_url && callback_url.empty?
|
||||||
|
callback_url = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if callback_url
|
||||||
|
callback_url = URI.parse(callback_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
if sid = env.get?("sid").try &.as(String)
|
||||||
|
env.response.content_type = "text/html"
|
||||||
|
|
||||||
|
csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY, PG_DB, use_nonce: true)
|
||||||
|
next templated "authorize_token"
|
||||||
|
else
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
|
||||||
|
superset_scopes = env.get("scopes").as(Array(String))
|
||||||
|
|
||||||
|
authorized_scopes = [] of String
|
||||||
|
scopes.each do |scope|
|
||||||
|
if scopes_include_scope(superset_scopes, scope)
|
||||||
|
authorized_scopes << scope
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
access_token = generate_token(user.email, authorized_scopes, expire, HMAC_KEY, PG_DB)
|
||||||
|
|
||||||
|
if callback_url
|
||||||
|
access_token = URI.escape(access_token)
|
||||||
|
|
||||||
|
if query = callback_url.query
|
||||||
|
query = HTTP::Params.parse(query.not_nil!)
|
||||||
|
else
|
||||||
|
query = HTTP::Params.new
|
||||||
|
end
|
||||||
|
|
||||||
|
query["token"] = access_token
|
||||||
|
callback_url.query = query.to_s
|
||||||
|
|
||||||
|
env.redirect callback_url.to_s
|
||||||
|
else
|
||||||
|
access_token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/api/v1/auth/tokens/unregister" do |env|
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
scopes = env.get("scopes").as(Array(String))
|
||||||
|
|
||||||
|
session = env.params.json["session"]?.try &.as(String)
|
||||||
|
session ||= env.get("session").as(String)
|
||||||
|
|
||||||
|
# Allow tokens to revoke other tokens with correct scope
|
||||||
|
if session == env.get("session").as(String)
|
||||||
|
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", session)
|
||||||
|
elsif scopes_include_scope(scopes, "GET:tokens")
|
||||||
|
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", session)
|
||||||
|
else
|
||||||
|
error_message = {"error" => "Cannot revoke session #{session}"}.to_json
|
||||||
|
env.response.status_code = 400
|
||||||
|
next error_message
|
||||||
|
end
|
||||||
|
|
||||||
|
env.response.status_code = 204
|
||||||
|
end
|
||||||
|
|
||||||
get "/api/manifest/dash/id/videoplayback" do |env|
|
get "/api/manifest/dash/id/videoplayback" do |env|
|
||||||
env.response.headers.delete("Content-Type")
|
env.response.headers.delete("Content-Type")
|
||||||
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
@ -4708,8 +4999,8 @@ error 404 do |env|
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Check if item is video ID
|
# Check if item is video ID
|
||||||
client = make_client(URI.parse("https://youtu.be"))
|
client = make_client(YT_URL)
|
||||||
if client.head("/#{item}").status_code != 404
|
if item.match(/^[a-zA-Z0-9_-]{11}$/) && client.head("/watch?v=#{item}").status_code != 404
|
||||||
env.response.headers["Location"] = url
|
env.response.headers["Location"] = url
|
||||||
halt env, status_code: 302
|
halt env, status_code: 302
|
||||||
end
|
end
|
||||||
@ -4760,9 +5051,11 @@ public_folder "assets"
|
|||||||
Kemal.config.powered_by_header = false
|
Kemal.config.powered_by_header = false
|
||||||
add_handler FilteredCompressHandler.new
|
add_handler FilteredCompressHandler.new
|
||||||
add_handler APIHandler.new
|
add_handler APIHandler.new
|
||||||
|
add_handler AuthHandler.new
|
||||||
add_handler DenyFrame.new
|
add_handler DenyFrame.new
|
||||||
add_context_storage_type(User)
|
add_context_storage_type(Array(String))
|
||||||
add_context_storage_type(Preferences)
|
add_context_storage_type(Preferences)
|
||||||
|
add_context_storage_type(User)
|
||||||
|
|
||||||
Kemal.config.logger = logger
|
Kemal.config.logger = logger
|
||||||
Kemal.run
|
Kemal.run
|
||||||
|
@ -20,7 +20,9 @@ module HTTP::Handler
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Kemal::RouteHandler
|
class Kemal::RouteHandler
|
||||||
exclude ["/api/v1/*"]
|
{% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %}
|
||||||
|
exclude ["/api/v1/*"], {{method}}
|
||||||
|
{% end %}
|
||||||
|
|
||||||
# Processes the route if it's a match. Otherwise renders 404.
|
# Processes the route if it's a match. Otherwise renders 404.
|
||||||
private def process_request(context)
|
private def process_request(context)
|
||||||
@ -37,7 +39,9 @@ class Kemal::RouteHandler
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Kemal::ExceptionHandler
|
class Kemal::ExceptionHandler
|
||||||
exclude ["/api/v1/*"]
|
{% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %}
|
||||||
|
exclude ["/api/v1/*"], {{method}}
|
||||||
|
{% end %}
|
||||||
|
|
||||||
private def call_exception_with_status_code(context : HTTP::Server::Context, exception : Exception, status_code : Int32)
|
private def call_exception_with_status_code(context : HTTP::Server::Context, exception : Exception, status_code : Int32)
|
||||||
return if context.response.closed?
|
return if context.response.closed?
|
||||||
@ -76,8 +80,59 @@ class FilteredCompressHandler < Kemal::Handler
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class AuthHandler < Kemal::Handler
|
||||||
|
{% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %}
|
||||||
|
only ["/api/v1/auth/*"], {{method}}
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
return call_next env unless only_match? env
|
||||||
|
|
||||||
|
begin
|
||||||
|
if token = env.request.headers["Authorization"]?
|
||||||
|
token = JSON.parse(URI.unescape(token.lchop("Bearer ")))
|
||||||
|
session = URI.unescape(token["session"].as_s)
|
||||||
|
scopes, expire, signature = validate_request(token, session, env.request, HMAC_KEY, PG_DB, nil)
|
||||||
|
|
||||||
|
if email = PG_DB.query_one?("SELECT email FROM session_ids WHERE id = $1", session, as: String)
|
||||||
|
user = PG_DB.query_one("SELECT * FROM users WHERE email = $1", email, as: User)
|
||||||
|
end
|
||||||
|
elsif sid = env.request.cookies["SID"]?.try &.value
|
||||||
|
if sid.starts_with? "v1:"
|
||||||
|
raise "Cannot use token as SID"
|
||||||
|
end
|
||||||
|
|
||||||
|
if email = PG_DB.query_one?("SELECT email FROM session_ids WHERE id = $1", sid, as: String)
|
||||||
|
user = PG_DB.query_one("SELECT * FROM users WHERE email = $1", email, as: User)
|
||||||
|
end
|
||||||
|
|
||||||
|
scopes = [":*"]
|
||||||
|
session = sid
|
||||||
|
end
|
||||||
|
|
||||||
|
if !user
|
||||||
|
raise "Request must be authenticated"
|
||||||
|
end
|
||||||
|
|
||||||
|
env.set "scopes", scopes
|
||||||
|
env.set "user", user
|
||||||
|
env.set "session", session
|
||||||
|
|
||||||
|
call_next env
|
||||||
|
rescue ex
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
|
||||||
|
error_message = {"error" => ex.message}.to_json
|
||||||
|
env.response.status_code = 403
|
||||||
|
env.response.puts error_message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class APIHandler < Kemal::Handler
|
class APIHandler < Kemal::Handler
|
||||||
only ["/api/v1/*"]
|
{% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %}
|
||||||
|
only ["/api/v1/*"], {{method}}
|
||||||
|
{% end %}
|
||||||
|
|
||||||
def call(env)
|
def call(env)
|
||||||
return call_next env unless only_match? env
|
return call_next env unless only_match? env
|
||||||
|
146
src/invidious/helpers/tokens.cr
Normal file
146
src/invidious/helpers/tokens.cr
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
def generate_token(email, scopes, expire, key, db)
|
||||||
|
session = "v1:#{Base64.urlsafe_encode(Random::Secure.random_bytes(32))}"
|
||||||
|
PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", session, email, Time.now)
|
||||||
|
|
||||||
|
token = {
|
||||||
|
"session" => session,
|
||||||
|
"scopes" => scopes,
|
||||||
|
"expire" => expire,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !expire
|
||||||
|
token.delete("expire")
|
||||||
|
end
|
||||||
|
|
||||||
|
token["signature"] = sign_token(key, token)
|
||||||
|
|
||||||
|
return token.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_response(session, scopes, key, db, expire = 6.hours, use_nonce = false)
|
||||||
|
expire = Time.now + expire
|
||||||
|
|
||||||
|
token = {
|
||||||
|
"session" => session,
|
||||||
|
"expire" => expire.to_unix,
|
||||||
|
"scopes" => scopes,
|
||||||
|
}
|
||||||
|
|
||||||
|
if use_nonce
|
||||||
|
nonce = Random::Secure.hex(16)
|
||||||
|
db.exec("INSERT INTO nonces VALUES ($1, $2) ON CONFLICT DO NOTHING", nonce, expire)
|
||||||
|
token["nonce"] = nonce
|
||||||
|
end
|
||||||
|
|
||||||
|
token["signature"] = sign_token(key, token)
|
||||||
|
|
||||||
|
return token.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def sign_token(key, hash)
|
||||||
|
string_to_sign = [] of String
|
||||||
|
|
||||||
|
hash.each do |key, value|
|
||||||
|
if key == "signature"
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
if value.is_a?(JSON::Any)
|
||||||
|
case value
|
||||||
|
when .as_a?
|
||||||
|
value = value.as_a.map { |item| item.as_s }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
case value
|
||||||
|
when Array
|
||||||
|
string_to_sign << "#{key}=#{value.sort.join(",")}"
|
||||||
|
when Tuple
|
||||||
|
string_to_sign << "#{key}=#{value.to_a.sort.join(",")}"
|
||||||
|
else
|
||||||
|
string_to_sign << "#{key}=#{value}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
string_to_sign = string_to_sign.sort.join("\n")
|
||||||
|
return Base64.urlsafe_encode(OpenSSL::HMAC.digest(:sha256, key, string_to_sign)).strip
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_request(token, session, request, key, db, locale = nil)
|
||||||
|
case token
|
||||||
|
when String
|
||||||
|
token = JSON.parse(URI.unescape(token)).as_h
|
||||||
|
when JSON::Any
|
||||||
|
token = token.as_h
|
||||||
|
when Nil
|
||||||
|
raise translate(locale, "Hidden field \"token\" is a required field")
|
||||||
|
end
|
||||||
|
|
||||||
|
if token["signature"] != sign_token(key, token)
|
||||||
|
raise translate(locale, "Invalid signature")
|
||||||
|
end
|
||||||
|
|
||||||
|
if token["session"] != session
|
||||||
|
raise translate(locale, "Invalid token")
|
||||||
|
end
|
||||||
|
|
||||||
|
if token["nonce"]? && (nonce = db.query_one?("SELECT * FROM nonces WHERE nonce = $1", token["nonce"], as: {String, Time}))
|
||||||
|
if nonce[1] > Time.now
|
||||||
|
db.exec("UPDATE nonces SET expire = $1 WHERE nonce = $2", Time.new(1990, 1, 1), nonce[0])
|
||||||
|
else
|
||||||
|
raise translate(locale, "Invalid token")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scopes = token["scopes"].as_a.map { |v| v.as_s }
|
||||||
|
scope = "#{request.method}:#{request.path.lchop("/api/v1/auth/").lstrip("/")}"
|
||||||
|
|
||||||
|
if !scopes_include_scope(scopes, scope)
|
||||||
|
raise translate(locale, "Invalid scope")
|
||||||
|
end
|
||||||
|
|
||||||
|
expire = token["expire"]?.try &.as_i
|
||||||
|
if expire.try &.< Time.now.to_unix
|
||||||
|
raise translate(locale, "Token is expired, please try again")
|
||||||
|
end
|
||||||
|
|
||||||
|
return {scopes, expire, token["signature"].as_s}
|
||||||
|
end
|
||||||
|
|
||||||
|
def scope_includes_scope(scope, subset)
|
||||||
|
methods, endpoint = scope.split(":")
|
||||||
|
methods = methods.split(";").map { |method| method.upcase }.reject { |method| method.empty? }.sort
|
||||||
|
endpoint = endpoint.downcase
|
||||||
|
|
||||||
|
subset_methods, subset_endpoint = subset.split(":")
|
||||||
|
subset_methods = subset_methods.split(";").map { |method| method.upcase }.sort
|
||||||
|
subset_endpoint = subset_endpoint.downcase
|
||||||
|
|
||||||
|
if methods.empty?
|
||||||
|
methods = %w(GET POST PUT HEAD DELETE PATCH OPTIONS)
|
||||||
|
end
|
||||||
|
|
||||||
|
if methods & subset_methods != subset_methods
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if endpoint.ends_with?("*") && !subset_endpoint.starts_with? endpoint.rchop("*")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if !endpoint.ends_with?("*") && subset_endpoint != endpoint
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
def scopes_include_scope(scopes, subset)
|
||||||
|
scopes.each do |scope|
|
||||||
|
if scope_includes_scope(scope, subset)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
@ -197,83 +197,6 @@ def create_user(sid, email, password)
|
|||||||
return user, sid
|
return user, sid
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_response(session, scopes, key, db, expire = 6.hours, use_nonce = false)
|
|
||||||
expire = Time.now + expire
|
|
||||||
|
|
||||||
token = {
|
|
||||||
"session" => session,
|
|
||||||
"expire" => expire.to_unix,
|
|
||||||
"scopes" => scopes,
|
|
||||||
}
|
|
||||||
|
|
||||||
if use_nonce
|
|
||||||
nonce = Random::Secure.hex(16)
|
|
||||||
db.exec("INSERT INTO nonces VALUES ($1, $2) ON CONFLICT DO NOTHING", nonce, expire)
|
|
||||||
token["nonce"] = nonce
|
|
||||||
end
|
|
||||||
|
|
||||||
token["signature"] = sign_token(key, token)
|
|
||||||
|
|
||||||
return token.to_json
|
|
||||||
end
|
|
||||||
|
|
||||||
def sign_token(key, hash)
|
|
||||||
string_to_sign = [] of String
|
|
||||||
|
|
||||||
hash.each do |key, value|
|
|
||||||
if key == "signature"
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
if value.is_a?(JSON::Any)
|
|
||||||
case value
|
|
||||||
when .as_a?
|
|
||||||
value = value.as_a.map { |item| item.as_s }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
case value
|
|
||||||
when Array
|
|
||||||
string_to_sign << "#{key}=#{value.sort.join(",")}"
|
|
||||||
when Tuple
|
|
||||||
string_to_sign << "#{key}=#{value.to_a.sort.join(",")}"
|
|
||||||
else
|
|
||||||
string_to_sign << "#{key}=#{value}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
string_to_sign = string_to_sign.sort.join("\n")
|
|
||||||
return Base64.urlsafe_encode(OpenSSL::HMAC.digest(:sha256, key, string_to_sign)).strip
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_response(token, session, scope, key, db, locale)
|
|
||||||
if !token
|
|
||||||
raise translate(locale, "Hidden field \"token\" is a required field")
|
|
||||||
end
|
|
||||||
|
|
||||||
token = JSON.parse(URI.unescape(token)).as_h
|
|
||||||
|
|
||||||
if token["signature"]? != sign_token(key, token)
|
|
||||||
raise translate(locale, "Invalid token")
|
|
||||||
end
|
|
||||||
|
|
||||||
if token["nonce"]? && (nonce = db.query_one?("SELECT * FROM nonces WHERE nonce = $1", token["nonce"], as: {String, Time}))
|
|
||||||
if nonce[1] > Time.now
|
|
||||||
db.exec("UPDATE nonces SET expire = $1 WHERE nonce = $2", Time.new(1990, 1, 1), nonce[0])
|
|
||||||
else
|
|
||||||
raise translate(locale, "Invalid token")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if !token["scopes"].as_a.includes? scope.strip("/")
|
|
||||||
raise translate(locale, "Invalid token")
|
|
||||||
end
|
|
||||||
|
|
||||||
if token["expire"].as_i < Time.now.to_unix
|
|
||||||
raise translate(locale, "Token is expired, please try again")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate_captcha(key, db)
|
def generate_captcha(key, db)
|
||||||
second = Random::Secure.rand(12)
|
second = Random::Secure.rand(12)
|
||||||
second_angle = second * 30
|
second_angle = second * 30
|
||||||
@ -326,7 +249,7 @@ def generate_captcha(key, db)
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
question: image,
|
question: image,
|
||||||
tokens: {create_response(answer, {"login"}, key, db, use_nonce: true)},
|
tokens: {generate_response(answer, {":login"}, key, db, use_nonce: true)},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -335,7 +258,7 @@ def generate_text_captcha(key, db)
|
|||||||
response = JSON.parse(response)
|
response = JSON.parse(response)
|
||||||
|
|
||||||
tokens = response["a"].as_a.map do |answer|
|
tokens = response["a"].as_a.map do |answer|
|
||||||
create_response(answer.as_s, {"login"}, key, db, use_nonce: true)
|
generate_response(answer.as_s, {":login"}, key, db, use_nonce: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -486,15 +486,15 @@ struct Video
|
|||||||
if storyboard = storyboards.try &.["spec"]?
|
if storyboard = storyboards.try &.["spec"]?
|
||||||
.try &.as_s
|
.try &.as_s
|
||||||
return [{
|
return [{
|
||||||
url: storyboard.split("#")[0],
|
url: storyboard.split("#")[0],
|
||||||
width: 106,
|
width: 106,
|
||||||
height: 60,
|
height: 60,
|
||||||
count: -1,
|
count: -1,
|
||||||
interval: 5000,
|
interval: 5000,
|
||||||
storyboard_width: 3,
|
storyboard_width: 3,
|
||||||
storyboard_height: 3,
|
storyboard_height: 3,
|
||||||
storyboard_count: -1,
|
storyboard_count: -1,
|
||||||
}]
|
}]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
78
src/invidious/views/authorize_token.ecr
Normal file
78
src/invidious/views/authorize_token.ecr
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= translate(locale, "Token") %> - Invidious</title>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if env.get? "access_token" %>
|
||||||
|
<div class="pure-g h-box">
|
||||||
|
<div class="pure-u-1-3">
|
||||||
|
<h3>
|
||||||
|
<%= translate(locale, "Token") %>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-3" style="text-align:center">
|
||||||
|
<h3>
|
||||||
|
<a href="/token_manager"><%= translate(locale, "Token manager") %></a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-3" style="text-align:right">
|
||||||
|
<h3>
|
||||||
|
<a href="/preferences"><%= translate(locale, "Preferences") %></a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-box">
|
||||||
|
<h4 style="padding-left:0.5em">
|
||||||
|
<code><%= env.get "access_token" %></code>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div class="h-box">
|
||||||
|
<form class="pure-form pure-form-aligned" action="/authorize_token" method="post">
|
||||||
|
<% if callback_url %>
|
||||||
|
<legend><%= translate(locale, "Authorize token for `x`?", "#{callback_url.scheme}://#{callback_url.host}") %></legend>
|
||||||
|
<% else %>
|
||||||
|
<legend><%= translate(locale, "Authorize token?") %></legend>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<div class="pure-u-1">
|
||||||
|
<ul>
|
||||||
|
<% scopes.each do |scope| %>
|
||||||
|
<li><%= scope %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<div class="pure-u-1-2">
|
||||||
|
<button type="submit" name="submit" value="clear_watch_history" class="pure-button pure-button-primary">
|
||||||
|
<%= translate(locale, "Yes") %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-2">
|
||||||
|
<% if callback_url %>
|
||||||
|
<a class="pure-button" href="<%= callback_url %>">
|
||||||
|
<% else %>
|
||||||
|
<a class="pure-button" href="/">
|
||||||
|
<% end %>
|
||||||
|
<%= translate(locale, "No") %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% scopes.each_with_index do |scope, i| %>
|
||||||
|
<input type="hidden" name="scopes[<%= i %>]" value="<%= scope %>">
|
||||||
|
<% end %>
|
||||||
|
<% if callback_url %>
|
||||||
|
<input type="hidden" name="callbackUrl" value="<%= callback_url %>">
|
||||||
|
<% end %>
|
||||||
|
<% if expire %>
|
||||||
|
<input type="hidden" name="expire" value="<%= expire %>">
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= URI.escape(csrf_token) %>">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
@ -19,6 +19,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" name="token" value="<%= URI.escape(token) %>">
|
<input type="hidden" name="csrf_token" value="<%= URI.escape(csrf_token) %>">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -86,7 +86,7 @@
|
|||||||
<img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
|
<img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
|
||||||
<% if env.get? "show_watched" %>
|
<% if env.get? "show_watched" %>
|
||||||
<form onsubmit="return false;" action="/watch_ajax?action_mark_watched=1&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post">
|
<form onsubmit="return false;" action="/watch_ajax?action_mark_watched=1&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post">
|
||||||
<input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>">
|
<input type="hidden" name="csrf_token" value="<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||||
<p class="watched">
|
<p class="watched">
|
||||||
<a onclick="mark_watched(this)" data-id="<%= item.id %>" href="#">
|
<a onclick="mark_watched(this)" data-id="<%= item.id %>" href="#">
|
||||||
<button type="submit" style="all:unset">
|
<button type="submit" style="all:unset">
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<% if subscriptions.includes? ucid %>
|
<% if subscriptions.includes? ucid %>
|
||||||
<p>
|
<p>
|
||||||
<form onsubmit="return false;" action="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
|
<form onsubmit="return false;" action="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
|
||||||
<input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>">
|
<input type="hidden" name="csrf_token" value="<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||||
<a id="subscribe" onclick="unsubscribe()" class="pure-button pure-button-primary" href="#">
|
<a id="subscribe" onclick="unsubscribe()" class="pure-button pure-button-primary" href="#">
|
||||||
<b><input style="all:unset" type="submit" value="<%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %>"></b>
|
<b><input style="all:unset" type="submit" value="<%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %>"></b>
|
||||||
</a>
|
</a>
|
||||||
@ -11,7 +11,7 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<p>
|
<p>
|
||||||
<form onsubmit="return false;" action="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
|
<form onsubmit="return false;" action="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
|
||||||
<input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>">
|
<input type="hidden" name="csrf_token" value="<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||||
<a id="subscribe" onclick="subscribe()" class="pure-button pure-button-primary" href="#">
|
<a id="subscribe" onclick="subscribe()" class="pure-button pure-button-primary" href="#">
|
||||||
<b><input style="all:unset" type="submit" value="<%= translate(locale, "Subscribe") %> | <%= sub_count_text %>"></b>
|
<b><input style="all:unset" type="submit" value="<%= translate(locale, "Subscribe") %> | <%= sub_count_text %>"></b>
|
||||||
</a>
|
</a>
|
||||||
|
@ -17,7 +17,7 @@ function subscribe(timeouts = 0) {
|
|||||||
xhr.timeout = 20000;
|
xhr.timeout = 20000;
|
||||||
xhr.open("POST", url, true);
|
xhr.open("POST", url, true);
|
||||||
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||||
xhr.send("token=<%= URI.escape(env.get?("token").try &.as(String) || "") %>");
|
xhr.send("csrf_token=<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>");
|
||||||
|
|
||||||
var fallback = subscribe_button.innerHTML;
|
var fallback = subscribe_button.innerHTML;
|
||||||
subscribe_button.onclick = unsubscribe;
|
subscribe_button.onclick = unsubscribe;
|
||||||
@ -53,7 +53,7 @@ function unsubscribe(timeouts = 0) {
|
|||||||
xhr.timeout = 20000;
|
xhr.timeout = 20000;
|
||||||
xhr.open("POST", url, true);
|
xhr.open("POST", url, true);
|
||||||
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||||
xhr.send("token=<%= URI.escape(env.get?("token").try &.as(String) || "") %>");
|
xhr.send("csrf_token=<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>");
|
||||||
|
|
||||||
var fallback = subscribe_button.innerHTML;
|
var fallback = subscribe_button.innerHTML;
|
||||||
subscribe_button.onclick = subscribe;
|
subscribe_button.onclick = subscribe;
|
||||||
|
@ -19,6 +19,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" name="token" value="<%= URI.escape(token) %>">
|
<input type="hidden" name="csrf_token" value="<%= URI.escape(csrf_token) %>">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
<div class="thumbnail">
|
<div class="thumbnail">
|
||||||
<img class="thumbnail" src="/vi/<%= item %>/mqdefault.jpg"/>
|
<img class="thumbnail" src="/vi/<%= item %>/mqdefault.jpg"/>
|
||||||
<form onsubmit="return false;" action="/watch_ajax?action_mark_unwatched=1&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post">
|
<form onsubmit="return false;" action="/watch_ajax?action_mark_unwatched=1&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post">
|
||||||
<input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>">
|
<input type="hidden" name="csrf_token" value="<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||||
<p class="watched">
|
<p class="watched">
|
||||||
<a onclick="mark_unwatched(this)" data-id="<%= item %>" href="#">
|
<a onclick="mark_unwatched(this)" data-id="<%= item %>" href="#">
|
||||||
<button type="submit" style="all:unset">
|
<button type="submit" style="all:unset">
|
||||||
@ -61,7 +61,7 @@ function mark_unwatched(target) {
|
|||||||
xhr.timeout = 20000;
|
xhr.timeout = 20000;
|
||||||
xhr.open("POST", url, true);
|
xhr.open("POST", url, true);
|
||||||
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||||
xhr.send("token=<%= URI.escape(env.get?("token").try &.as(String) || "") %>");
|
xhr.send("csrf_token=<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>");
|
||||||
|
|
||||||
xhr.onreadystatechange = function() {
|
xhr.onreadystatechange = function() {
|
||||||
if (xhr.readyState == 4) {
|
if (xhr.readyState == 4) {
|
||||||
|
@ -216,6 +216,10 @@ function update_value(element) {
|
|||||||
<a href="/subscription_manager"><%= translate(locale, "Manage subscriptions") %></a>
|
<a href="/subscription_manager"><%= translate(locale, "Manage subscriptions") %></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<a href="/token_manager"><%= translate(locale, "Manage tokens") %></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<a href="/feed/history"><%= translate(locale, "Watch history") %></a>
|
<a href="/feed/history"><%= translate(locale, "Watch history") %></a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,12 +8,12 @@
|
|||||||
<a href="/feed/subscriptions"><%= translate(locale, "`x` subscriptions", %(<span id="count">#{subscriptions.size}</span>)) %></a>
|
<a href="/feed/subscriptions"><%= translate(locale, "`x` subscriptions", %(<span id="count">#{subscriptions.size}</span>)) %></a>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1-3" style="text-align:center;">
|
<div class="pure-u-1-3" style="text-align:center">
|
||||||
<h3>
|
<h3>
|
||||||
<a href="/feed/history"><%= translate(locale, "Watch history") %></a>
|
<a href="/feed/history"><%= translate(locale, "Watch history") %></a>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1-3" style="text-align:right;">
|
<div class="pure-u-1-3" style="text-align:right">
|
||||||
<h3>
|
<h3>
|
||||||
<a href="/data_control?referer=<%= referer %>"><%= translate(locale, "Import/Export") %></a>
|
<a href="/data_control?referer=<%= referer %>"><%= translate(locale, "Import/Export") %></a>
|
||||||
</h3>
|
</h3>
|
||||||
@ -22,17 +22,17 @@
|
|||||||
|
|
||||||
<% subscriptions.each do |channel| %>
|
<% subscriptions.each do |channel| %>
|
||||||
<div class="h-box">
|
<div class="h-box">
|
||||||
<div class="pure-g<% if channel.deleted %> deleted <% end%>">
|
<div class="pure-g<% if channel.deleted %> deleted <% end %>">
|
||||||
<div class="pure-u-2-5">
|
<div class="pure-u-2-5">
|
||||||
<h3 style="padding-left: 0.5em">
|
<h3 style="padding-left:0.5em">
|
||||||
<a href="/channel/<%= channel.id %>"><%= channel.author %></a>
|
<a href="/channel/<%= channel.id %>"><%= channel.author %></a>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-2-5"></div>
|
<div class="pure-u-2-5"></div>
|
||||||
<div class="pure-u-1-5" style="text-align: right;">
|
<div class="pure-u-1-5" style="text-align:right">
|
||||||
<h3 style="padding-right: 0.5em">
|
<h3 style="padding-right:0.5em">
|
||||||
<form onsubmit="return false;" action="/subscription_ajax?action_remove_subscriptions=1&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post">
|
<form onsubmit="return false" action="/subscription_ajax?action_remove_subscriptions=1&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post">
|
||||||
<input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>">
|
<input type="hidden" name="csrf_token" value="<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||||
<a onclick="remove_subscription(this)" data-ucid="<%= channel.id %>" href="#">
|
<a onclick="remove_subscription(this)" data-ucid="<%= channel.id %>" href="#">
|
||||||
<input style="all:unset" type="submit" value="<%= translate(locale, "unsubscribe") %>">
|
<input style="all:unset" type="submit" value="<%= translate(locale, "unsubscribe") %>">
|
||||||
</a>
|
</a>
|
||||||
@ -60,7 +60,7 @@ function remove_subscription(target) {
|
|||||||
xhr.timeout = 20000;
|
xhr.timeout = 20000;
|
||||||
xhr.open("POST", url, true);
|
xhr.open("POST", url, true);
|
||||||
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||||
xhr.send("token=<%= URI.escape(env.get?("token").try &.as(String) || "") %>");
|
xhr.send("csrf_token=<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>");
|
||||||
|
|
||||||
xhr.onreadystatechange = function() {
|
xhr.onreadystatechange = function() {
|
||||||
if (xhr.readyState == 4) {
|
if (xhr.readyState == 4) {
|
||||||
|
@ -62,7 +62,7 @@ function mark_watched(target) {
|
|||||||
xhr.timeout = 20000;
|
xhr.timeout = 20000;
|
||||||
xhr.open("POST", url, true);
|
xhr.open("POST", url, true);
|
||||||
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||||
xhr.send("token=<%= URI.escape(env.get?("token").try &.as(String) || "") %>");
|
xhr.send("csrf_token=<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>");
|
||||||
|
|
||||||
xhr.onreadystatechange = function() {
|
xhr.onreadystatechange = function() {
|
||||||
if (xhr.readyState == 4) {
|
if (xhr.readyState == 4) {
|
||||||
|
@ -70,7 +70,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1-4">
|
<div class="pure-u-1-4">
|
||||||
<form action="/signout?referer=<%= env.get?("current_page") %>" method="post">
|
<form action="/signout?referer=<%= env.get?("current_page") %>" method="post">
|
||||||
<input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>">
|
<input type="hidden" name="csrf_token" value="<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||||
<a class="pure-menu-heading" href="#">
|
<a class="pure-menu-heading" href="#">
|
||||||
<input style="all:unset" type="submit" value="<%= translate(locale, "Sign out") %>">
|
<input style="all:unset" type="submit" value="<%= translate(locale, "Sign out") %>">
|
||||||
</a>
|
</a>
|
||||||
|
72
src/invidious/views/token_manager.ecr
Normal file
72
src/invidious/views/token_manager.ecr
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= translate(locale, "Token manager") %> - Invidious</title>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="pure-g h-box">
|
||||||
|
<div class="pure-u-1-3">
|
||||||
|
<h3>
|
||||||
|
<%= translate(locale, "`x` tokens", %(<span id="count">#{tokens.size}</span>)) %>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-3"></div>
|
||||||
|
<div class="pure-u-1-3" style="text-align:right">
|
||||||
|
<h3>
|
||||||
|
<a href="/preferences?referer=<%= referer %>"><%= translate(locale, "Preferences") %></a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% tokens.each do |token| %>
|
||||||
|
<div class="h-box">
|
||||||
|
<div class="pure-g<% if token[:session] == sid %> deleted <% end %>">
|
||||||
|
<div class="pure-u-3-5">
|
||||||
|
<h4 style="padding-left:0.5em">
|
||||||
|
<code><%= token[:session] %></code>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-5" style="text-align:center">
|
||||||
|
<h4><%= translate(locale, "`x` ago", recode_date(token[:issued], locale)) %></h4>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-5" style="text-align:right">
|
||||||
|
<h3 style="padding-right:0.5em">
|
||||||
|
<form onsubmit="return false" action="/token_ajax?action_revoke_token=1&session=<%= token[:session] %>&referer=<%= env.get("current_page") %>" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||||
|
<a onclick="revoke_token(this)" data-session="<%= token[:session] %>" href="#">
|
||||||
|
<input style="all:unset" type="submit" value="<%= translate(locale, "revoke") %>">
|
||||||
|
</a>
|
||||||
|
</form>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if tokens[-1].try &.[:session]? != token[:session] %>
|
||||||
|
<hr>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function revoke_token(target) {
|
||||||
|
var row = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||||
|
row.style.display = "none";
|
||||||
|
var count = document.getElementById("count")
|
||||||
|
count.innerText = count.innerText - 1;
|
||||||
|
|
||||||
|
var url = "/token_ajax?action_revoke_token=1&redirect=false&referer=<%= env.get("current_page") %>&session=" + target.getAttribute("data-session");
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.responseType = "json";
|
||||||
|
xhr.timeout = 20000;
|
||||||
|
xhr.open("POST", url, true);
|
||||||
|
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||||
|
xhr.send("csrf_token=<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>");
|
||||||
|
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState == 4) {
|
||||||
|
if (xhr.status != 200) {
|
||||||
|
count.innerText = count.innerText - 1 + 2;
|
||||||
|
row.style.display = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
Loading…
Reference in New Issue
Block a user