mirror of
https://github.com/iv-org/invidious.git
synced 2025-01-03 21:51:45 +05:30
Merge 46fef734b4
into 701f03d5e8
This commit is contained in:
commit
464034bc3e
@ -13,6 +13,7 @@ CREATE TABLE IF NOT EXISTS public.users
|
|||||||
token text,
|
token text,
|
||||||
watched text[],
|
watched text[],
|
||||||
feed_needs_update boolean,
|
feed_needs_update boolean,
|
||||||
|
totp_secret VARCHAR(128)
|
||||||
CONSTRAINT users_email_key UNIQUE (email)
|
CONSTRAINT users_email_key UNIQUE (email)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -484,5 +484,16 @@
|
|||||||
"channel_tab_releases_label": "Releases",
|
"channel_tab_releases_label": "Releases",
|
||||||
"channel_tab_playlists_label": "Playlists",
|
"channel_tab_playlists_label": "Playlists",
|
||||||
"channel_tab_community_label": "Community",
|
"channel_tab_community_label": "Community",
|
||||||
"channel_tab_channels_label": "Channels"
|
"channel_tab_channels_label": "Channels",
|
||||||
}
|
"setup_totp_form_header": "Setup two-factor authentication (TOTP)",
|
||||||
|
"setup_totp_instructions_download_auth": "Install an authenticator app (or anything that supports TOTP) on your device",
|
||||||
|
"setup_totp_instructions_enter_code": "Enter the following <strong>secret</strong> code:",
|
||||||
|
"setup_totp_instructions_validate_code": "Enter the 6 digit number on your screen. Be sure to do it under thirty seconds!",
|
||||||
|
"setup_totp_submit_button": "Setup TOTP",
|
||||||
|
"general_totp_empty_field": "The TOTP code is a required field",
|
||||||
|
"general_totp_invalid_code": "The TOTP code entered is invalid",
|
||||||
|
"general_totp_enter_code_field": "6 digit number",
|
||||||
|
"general_totp_enter_code_header": "Two-factor authentication",
|
||||||
|
"general_totp_verify_button": "Verify",
|
||||||
|
"remove_totp_header": "Remove two-factor authentication (TOTP)",
|
||||||
|
"remove_totp_confirm_message": "Are you sure you would like to remove two-factor-authentication?"}
|
||||||
|
21
shard.lock
21
shard.lock
@ -1,12 +1,24 @@
|
|||||||
version: 2.0
|
version: 2.0
|
||||||
shards:
|
shards:
|
||||||
|
ameba:
|
||||||
|
git: https://github.com/crystal-ameba/ameba.git
|
||||||
|
version: 0.14.4
|
||||||
|
|
||||||
athena-negotiation:
|
athena-negotiation:
|
||||||
git: https://github.com/athena-framework/negotiation.git
|
git: https://github.com/athena-framework/negotiation.git
|
||||||
version: 0.1.1
|
version: 0.1.3
|
||||||
|
|
||||||
backtracer:
|
backtracer:
|
||||||
git: https://github.com/sija/backtracer.cr.git
|
git: https://github.com/sija/backtracer.cr.git
|
||||||
version: 1.2.1
|
version: 1.2.2
|
||||||
|
|
||||||
|
base32:
|
||||||
|
git: https://github.com/philnash/base32.git
|
||||||
|
version: 0.1.1+git.commit.0a21c1d90731fdefcb3f0db4913f49d3d25350ac
|
||||||
|
|
||||||
|
crotp:
|
||||||
|
git: https://github.com/philnash/crotp.git
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
db:
|
db:
|
||||||
git: https://github.com/crystal-lang/crystal-db.git
|
git: https://github.com/crystal-lang/crystal-db.git
|
||||||
@ -38,12 +50,9 @@ shards:
|
|||||||
|
|
||||||
spectator:
|
spectator:
|
||||||
git: https://github.com/icy-arctic-fox/spectator.git
|
git: https://github.com/icy-arctic-fox/spectator.git
|
||||||
version: 0.10.4
|
version: 0.10.6
|
||||||
|
|
||||||
sqlite3:
|
sqlite3:
|
||||||
git: https://github.com/crystal-lang/crystal-sqlite3.git
|
git: https://github.com/crystal-lang/crystal-sqlite3.git
|
||||||
version: 0.18.0
|
version: 0.18.0
|
||||||
|
|
||||||
ameba:
|
|
||||||
git: https://github.com/crystal-ameba/ameba.git
|
|
||||||
version: 0.14.3
|
|
||||||
|
@ -28,6 +28,9 @@ dependencies:
|
|||||||
athena-negotiation:
|
athena-negotiation:
|
||||||
github: athena-framework/negotiation
|
github: athena-framework/negotiation
|
||||||
version: ~> 0.1.1
|
version: ~> 0.1.1
|
||||||
|
crotp:
|
||||||
|
github: philnash/crotp
|
||||||
|
version: ~> 1.0.0
|
||||||
|
|
||||||
development_dependencies:
|
development_dependencies:
|
||||||
spectator:
|
spectator:
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
module Invidious::Database::Migrations
|
||||||
|
class AddTotpSecretToUsersTable < Migration
|
||||||
|
version 11
|
||||||
|
|
||||||
|
def up(conn : DB::Connection)
|
||||||
|
conn.exec <<-SQL
|
||||||
|
ALTER TABLE users ADD COLUMN totp_secret VARCHAR(128)
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -445,3 +445,14 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
|
|||||||
end
|
end
|
||||||
return text
|
return text
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Templates the 2fa validator page.
|
||||||
|
#
|
||||||
|
# Requires the env, user, sid and locale variables for
|
||||||
|
# generating a csrf_token and the required variables for the view.
|
||||||
|
def call_totp_validator(env, user, sid, locale)
|
||||||
|
referer = URI.decode_www_form(env.get?("current_page").to_s)
|
||||||
|
csrf_token = generate_response(sid, {":2fa/validate"}, HMAC_KEY)
|
||||||
|
email, password = {user.email, nil}
|
||||||
|
return templated "user/validate_2fa"
|
||||||
|
end
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{% skip_file if flag?(:api_only) %}
|
{% skip_file if flag?(:api_only) %}
|
||||||
|
|
||||||
|
require "crotp"
|
||||||
|
|
||||||
module Invidious::Routes::Account
|
module Invidious::Routes::Account
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
@ -21,6 +23,11 @@ module Invidious::Routes::Account
|
|||||||
|
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
sid = sid.as(String)
|
sid = sid.as(String)
|
||||||
|
|
||||||
|
if user.totp_secret && env.request.cookies["2faVerified"]?.try &.value != "1" || nil
|
||||||
|
return call_totp_validator(env, user, sid, locale)
|
||||||
|
end
|
||||||
|
|
||||||
csrf_token = generate_response(sid, {":change_password"}, HMAC_KEY)
|
csrf_token = generate_response(sid, {":change_password"}, HMAC_KEY)
|
||||||
|
|
||||||
templated "user/change_password"
|
templated "user/change_password"
|
||||||
@ -96,6 +103,11 @@ module Invidious::Routes::Account
|
|||||||
|
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
sid = sid.as(String)
|
sid = sid.as(String)
|
||||||
|
|
||||||
|
if user.totp_secret && env.request.cookies["2faVerified"]?.try &.value != "1" || nil
|
||||||
|
return call_totp_validator(env, user, sid, locale)
|
||||||
|
end
|
||||||
|
|
||||||
csrf_token = generate_response(sid, {":delete_account"}, HMAC_KEY)
|
csrf_token = generate_response(sid, {":delete_account"}, HMAC_KEY)
|
||||||
|
|
||||||
templated "user/delete_account"
|
templated "user/delete_account"
|
||||||
@ -195,14 +207,20 @@ module Invidious::Routes::Account
|
|||||||
|
|
||||||
user = env.get? "user"
|
user = env.get? "user"
|
||||||
sid = env.get? "sid"
|
sid = env.get? "sid"
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
|
||||||
|
if user.totp_secret && env.request.cookies["2faVerified"]?.try &.value != "1" || nil
|
||||||
|
return call_totp_validator(env, user, sid, locale)
|
||||||
|
end
|
||||||
|
|
||||||
referer = get_referer(env)
|
referer = get_referer(env)
|
||||||
|
|
||||||
if !user
|
if !user
|
||||||
return env.redirect "/login?referer=#{URI.encode_path_segment(env.request.resource)}"
|
return env.redirect "/login?referer=#{URI.encode_path_segment(env.request.resource)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
user = user.as(User)
|
|
||||||
sid = sid.as(String)
|
|
||||||
csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY)
|
csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY)
|
||||||
|
|
||||||
scopes = env.params.query["scopes"]?.try &.split(",")
|
scopes = env.params.query["scopes"]?.try &.split(",")
|
||||||
@ -351,4 +369,195 @@ module Invidious::Routes::Account
|
|||||||
return "{}"
|
return "{}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# -------------------
|
||||||
|
# 2fa through OTP handling
|
||||||
|
# -------------------
|
||||||
|
|
||||||
|
# Templates the page to setup 2fa on an user account
|
||||||
|
def setup_2fa_page(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env, unroll: false)
|
||||||
|
|
||||||
|
if !user
|
||||||
|
return env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
csrf_token = generate_response(sid, {":2fa/setup"}, HMAC_KEY)
|
||||||
|
|
||||||
|
db_secret = Random::Secure.random_bytes(16).hexstring
|
||||||
|
totp = CrOTP::TOTP.new(db_secret)
|
||||||
|
user_secret = totp.base32_secret
|
||||||
|
|
||||||
|
return templated "user/setup_2fa"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handles requests to setup 2fa on an user account
|
||||||
|
def setup_2fa(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env, unroll: false)
|
||||||
|
|
||||||
|
if !user
|
||||||
|
return env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
token = env.params.body["csrf_token"]?
|
||||||
|
|
||||||
|
begin
|
||||||
|
validate_request(token, sid, env.request, HMAC_KEY, locale)
|
||||||
|
rescue ex
|
||||||
|
return error_template(400, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
totp_code = env.params.body["totp_code"]?
|
||||||
|
db_secret = env.params.body["db_secret"] # Must exist
|
||||||
|
if !totp_code
|
||||||
|
return error_template(401, translate(locale, "general-totp-empty-field"))
|
||||||
|
end
|
||||||
|
|
||||||
|
totp_instance = CrOTP::TOTP.new(db_secret)
|
||||||
|
if !totp_instance.verify(totp_code)
|
||||||
|
return error_template(401, translate(locale, "general-totp-invalid-code"))
|
||||||
|
end
|
||||||
|
|
||||||
|
PG_DB.exec("UPDATE users SET totp_secret = $1 WHERE email = $2", db_secret.to_s, user.email)
|
||||||
|
env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handles requests to validate a TOTP code on an user account
|
||||||
|
def validate_2fa(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
referer = get_referer(env, unroll: false)
|
||||||
|
|
||||||
|
email = env.params.body["email"]?.try &.downcase.byte_slice(0, 254)
|
||||||
|
password = env.params.body["password"]?
|
||||||
|
totp_code = env.params.body["totp_code"]?
|
||||||
|
# This endpoint is only called when the user has a totp_secret.
|
||||||
|
user = PG_DB.query_one?("SELECT * FROM users WHERE email = $1", email, as: User).not_nil!
|
||||||
|
|
||||||
|
if !totp_code
|
||||||
|
return error_template(401, translate(locale, "general-totp-empty-field"))
|
||||||
|
end
|
||||||
|
|
||||||
|
totp_instance = CrOTP::TOTP.new(user.totp_secret.not_nil!)
|
||||||
|
if !totp_instance.verify(totp_code)
|
||||||
|
return error_template(401, translate(locale, "general-totp-invalid-code"))
|
||||||
|
end
|
||||||
|
|
||||||
|
if Kemal.config.ssl || CONFIG.https_only
|
||||||
|
secure = true
|
||||||
|
else
|
||||||
|
secure = false
|
||||||
|
end
|
||||||
|
|
||||||
|
#
|
||||||
|
# The validate_2fa method is used in two cases:
|
||||||
|
# 1. To authenticate the user when logging in
|
||||||
|
# 2. To verify that the user wishes to proceed with a dangerous action.
|
||||||
|
#
|
||||||
|
# As we've verified that the totp given is correct we can now proceed with
|
||||||
|
# authenticating and/or redirecting the user back to where they came from
|
||||||
|
#
|
||||||
|
|
||||||
|
logging_in = (email && password)
|
||||||
|
|
||||||
|
if logging_in
|
||||||
|
# Authenticate the user. The rest follows the code in login.cr
|
||||||
|
if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.not_nil!.byte_slice(0, 55))
|
||||||
|
#
|
||||||
|
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
|
||||||
|
PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.utc)
|
||||||
|
|
||||||
|
if CONFIG.domain
|
||||||
|
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: "#{CONFIG.domain}", value: sid, expires: Time.utc + 2.years,
|
||||||
|
secure: secure, http_only: true, path: "/")
|
||||||
|
else
|
||||||
|
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.utc + 2.years,
|
||||||
|
secure: secure, http_only: true, path: "/")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return error_template(401, "Wrong username or password")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Since this user has already registered, we don't want to overwrite their preferences
|
||||||
|
if env.request.cookies["PREFS"]?
|
||||||
|
cookie = env.request.cookies["PREFS"]
|
||||||
|
cookie.expires = Time.utc(1990, 1, 1)
|
||||||
|
env.response.cookies << cookie
|
||||||
|
end
|
||||||
|
|
||||||
|
env.redirect referer
|
||||||
|
else
|
||||||
|
token = env.params.body["csrf_token"]
|
||||||
|
|
||||||
|
begin
|
||||||
|
validate_request(token, env.get?("sid").as(String), env.request, HMAC_KEY, locale)
|
||||||
|
rescue ex
|
||||||
|
return error_template(400, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
if CONFIG.domain
|
||||||
|
env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", domain: "#{CONFIG.domain}", value: "1", expires: Time.utc + 5.minutes, secure: secure, http_only: true, path: "/")
|
||||||
|
else
|
||||||
|
env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", value: "1", expires: Time.utc + 5.minutes, secure: secure, http_only: true, path: "/")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
# Templates the page to remove 2fa on an user account
|
||||||
|
def remove_2fa_page(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env, unroll: false)
|
||||||
|
|
||||||
|
if !user || user.is_a? User && !user.totp_secret
|
||||||
|
return env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
csrf_token = generate_response(sid, {":2fa/remove"}, HMAC_KEY)
|
||||||
|
|
||||||
|
return templated "user/remove_2fa"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handles requests to remove 2fa on an user account
|
||||||
|
def remove_2fa(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env, unroll: false)
|
||||||
|
|
||||||
|
if !user || user.is_a? User && !user.totp_secret
|
||||||
|
return env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
token = env.params.body["csrf_token"]?
|
||||||
|
|
||||||
|
begin
|
||||||
|
validate_request(token, sid, env.request, HMAC_KEY, locale)
|
||||||
|
rescue ex
|
||||||
|
return error_template(400, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
PG_DB.exec("UPDATE users SET totp_secret = $1 WHERE email = $2", nil, user.email)
|
||||||
|
env.redirect referer
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -57,6 +57,12 @@ module Invidious::Routes::Login
|
|||||||
|
|
||||||
if user
|
if user
|
||||||
if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55))
|
if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55))
|
||||||
|
# If the password is correct then we'll go ahead and begin 2fa if applicable
|
||||||
|
if user.totp_secret
|
||||||
|
csrf_token = nil # setting this to nil for compatibility reasons.
|
||||||
|
return templated "user/validate_2fa"
|
||||||
|
end
|
||||||
|
|
||||||
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
|
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
|
||||||
Invidious::Database::SessionIDs.insert(sid, email)
|
Invidious::Database::SessionIDs.insert(sid, email)
|
||||||
|
|
||||||
|
@ -78,6 +78,13 @@ module Invidious::Routing
|
|||||||
post "/token_ajax", Routes::Account, :token_ajax
|
post "/token_ajax", Routes::Account, :token_ajax
|
||||||
post "/subscription_ajax", Routes::Subscriptions, :toggle_subscription
|
post "/subscription_ajax", Routes::Subscriptions, :toggle_subscription
|
||||||
get "/subscription_manager", Routes::Subscriptions, :subscription_manager
|
get "/subscription_manager", Routes::Subscriptions, :subscription_manager
|
||||||
|
|
||||||
|
# 2fa routes
|
||||||
|
Invidious::Routing.get "/2fa/setup", Routes::Account, :setup_2fa_page
|
||||||
|
Invidious::Routing.post "/2fa/setup", Routes::Account, :setup_2fa
|
||||||
|
Invidious::Routing.get "/2fa/remove", Routes::Account, :remove_2fa_page
|
||||||
|
Invidious::Routing.post "/2fa/remove", Routes::Account, :remove_2fa
|
||||||
|
Invidious::Routing.post "/2fa/validate", Routes::Account, :validate_2fa
|
||||||
end
|
end
|
||||||
|
|
||||||
def register_iv_playlist_routes
|
def register_iv_playlist_routes
|
||||||
|
@ -7,6 +7,7 @@ struct Invidious::User
|
|||||||
property notifications : Array(String)
|
property notifications : Array(String)
|
||||||
property subscriptions : Array(String)
|
property subscriptions : Array(String)
|
||||||
property email : String
|
property email : String
|
||||||
|
property totp_secret : String?
|
||||||
|
|
||||||
@[DB::Field(converter: Invidious::User::PreferencesConverter)]
|
@[DB::Field(converter: Invidious::User::PreferencesConverter)]
|
||||||
property preferences : Preferences
|
property preferences : Preferences
|
||||||
|
@ -17,6 +17,7 @@ def create_user(sid, email, password)
|
|||||||
token: token,
|
token: token,
|
||||||
watched: [] of String,
|
watched: [] of String,
|
||||||
feed_needs_update: true,
|
feed_needs_update: true,
|
||||||
|
totp_secret: nil,
|
||||||
})
|
})
|
||||||
|
|
||||||
return user, sid
|
return user, sid
|
||||||
|
@ -345,6 +345,14 @@
|
|||||||
<a href="/feed/history"><%= translate(locale, "Watch history") %></a>
|
<a href="/feed/history"><%= translate(locale, "Watch history") %></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<% if env.get?("user").as(User).totp_secret %>
|
||||||
|
<a href="/2fa/remove?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "remove_totp_header") %></a>
|
||||||
|
<% else %>
|
||||||
|
<a href="/2fa/setup?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "setup_totp_form_header") %></a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<a href="/delete_account?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Delete account") %></a>
|
<a href="/delete_account?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Delete account") %></a>
|
||||||
</div>
|
</div>
|
||||||
|
24
src/invidious/views/user/remove_2fa.ecr
Normal file
24
src/invidious/views/user/remove_2fa.ecr
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= translate(locale, "remove_totp_header") %> - Invidious</title>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="h-box">
|
||||||
|
<form class="pure-form pure-form-aligned" action="/2fa/remove?referer=<%= URI.encode_www_form(referer) %>" method="post">
|
||||||
|
<legend><%= translate(locale, "remove_totp_confirm_message") %></legend>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<div class="pure-u-1-2">
|
||||||
|
<button type="submit" name="submit" value="remove_2fa" class="pure-button pure-button-primary">
|
||||||
|
<%= translate(locale, "Yes") %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-2">
|
||||||
|
<a class="pure-button" href="<%= URI.encode_www_form(referer) %>">
|
||||||
|
<%= translate(locale, "No") %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(csrf_token) %>">
|
||||||
|
</form>
|
||||||
|
</div>
|
36
src/invidious/views/user/setup_2fa.ecr
Normal file
36
src/invidious/views/user/setup_2fa.ecr
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= translate(locale, "setup_totp_form_header") %> - Invidious</title>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5"></div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-3-5">
|
||||||
|
<div class="h-box">
|
||||||
|
<form class="pure-form pure-form-aligned" action="/2fa/setup?referer=<%= URI.encode_www_form(referer) %>" method="post">
|
||||||
|
<legend><%= translate(locale, "setup_totp_form_header") %></legend>
|
||||||
|
<fieldset>
|
||||||
|
|
||||||
|
<input name="db_secret" type="hidden" value="<%= HTML.escape(db_secret) %>">
|
||||||
|
|
||||||
|
<ol style="word-wrap: anywhere; white-space: break-space;">
|
||||||
|
<li> <%= translate(locale, "setup_totp_instructions_download_auth") %> </li>
|
||||||
|
<li> <%= translate(locale, "setup_totp_instructions_enter_code") %>
|
||||||
|
<code> <%=user_secret%> </code>
|
||||||
|
</li>
|
||||||
|
<li> <%= translate(locale, "setup_totp_instructions_validate_code") %> </li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<input required class="pure-input-1" name="totp_code" placeholder="<%= translate(locale, "general_totp_enter_code_field") %>">
|
||||||
|
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(csrf_token) %>">
|
||||||
|
|
||||||
|
<button type="submit" name="action" value="setup_totp_form_header" class="pure-button pure-button-primary">
|
||||||
|
<%= translate(locale, "setup_totp_submit_button") %>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5"></div>
|
||||||
|
</div>
|
37
src/invidious/views/user/validate_2fa.ecr
Normal file
37
src/invidious/views/user/validate_2fa.ecr
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= translate(locale, "setup_totp_form_header") %> - Invidious</title>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5"></div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-3-5">
|
||||||
|
<div class="h-box">
|
||||||
|
<form class="pure-form pure-form-aligned" action="/2fa/validate?referer=<%= URI.encode_www_form(referer) %>" method="post">
|
||||||
|
<legend><%= translate(locale, "general_totp_enter_code_header") %></legend>
|
||||||
|
<fieldset>
|
||||||
|
|
||||||
|
<!-- Hidden fields used for sign-in authentication-->
|
||||||
|
<% if email %>
|
||||||
|
<input name="email" type="hidden" value="<%= email %>">
|
||||||
|
<% end %>
|
||||||
|
<% if password %>
|
||||||
|
<input name="password" type="hidden" value="<%= HTML.escape(password) %>">
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<input required class="pure-input-1" name="totp_code" placeholder="<%= translate(locale, "general_totp_enter_code_field") %>">
|
||||||
|
|
||||||
|
<% if csrf_token %>
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(csrf_token) %>">
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
|
<button type="submit" name="action" class="pure-button pure-button-primary">
|
||||||
|
<%= translate(locale, "general_totp_verify_button") %>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5"></div>
|
||||||
|
</div>
|
Loading…
Reference in New Issue
Block a user