mirror of
https://github.com/System-End/identity-vault.git
synced 2026-04-20 00:25:21 +00:00
flavor dlavor (#110)
This commit is contained in:
parent
48f5e080b2
commit
d2dcc70e82
19 changed files with 207 additions and 46 deletions
|
|
@ -1,15 +1,17 @@
|
|||
class Components::AuthWelcome < Components::Base
|
||||
include Phlex::Rails::Helpers::DistanceOfTimeInWordsToNow
|
||||
|
||||
def initialize(headline:, subtitle:, return_to: nil, login_hint: nil)
|
||||
def initialize(headline:, subtitle:, return_to: nil, login_hint: nil, logo_path: nil)
|
||||
@headline = headline
|
||||
@subtitle = subtitle
|
||||
@return_to = return_to
|
||||
@login_hint = login_hint
|
||||
@logo_path = logo_path
|
||||
end
|
||||
|
||||
def view_template
|
||||
div(class: "auth-container") do
|
||||
render_brand if @logo_path
|
||||
div(class: "auth-card") do
|
||||
render_header
|
||||
render_actions
|
||||
|
|
@ -20,6 +22,14 @@ class Components::AuthWelcome < Components::Base
|
|||
|
||||
private
|
||||
|
||||
def render_brand
|
||||
div(class: "auth-brand") do
|
||||
vite_image_tag "images/hc-square.png", alt: "Hack Club logo", class: "brand-logo"
|
||||
span(class: "brand-plus") { "×" }
|
||||
vite_image_tag @logo_path, alt: "Logo", class: "brand-logo brand-logo--custom"
|
||||
end
|
||||
end
|
||||
|
||||
def render_header
|
||||
header do
|
||||
h1 { @headline }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
class Components::Brand < Components::Base
|
||||
def initialize(identity:)
|
||||
def initialize(identity:, logo_path: nil)
|
||||
@identity = identity
|
||||
@logo_path = logo_path
|
||||
end
|
||||
|
||||
def view_template
|
||||
|
|
@ -20,6 +21,12 @@ class Components::Brand < Components::Base
|
|||
end
|
||||
|
||||
def logo
|
||||
vite_image_tag "images/hc-square.png", alt: "Hack Club logo", class: "brand-logo"
|
||||
div(class: "brand-logos") do
|
||||
vite_image_tag "images/hc-square.png", alt: "Hack Club logo", class: "brand-logo"
|
||||
if @logo_path
|
||||
span(class: "brand-plus") { "×" }
|
||||
vite_image_tag @logo_path, alt: "Logo", class: "brand-logo brand-logo--custom"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ module PortalFlow
|
|||
included do
|
||||
before_action :validate_portal_return_url, only: [ :start, :portal ]
|
||||
before_action :store_return_url, only: [ :start, :portal ]
|
||||
helper_method :portal_onboarding_scenario
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -12,6 +13,16 @@ module PortalFlow
|
|||
session[:portal_return_to] || params[:return_to]
|
||||
end
|
||||
|
||||
def portal_program
|
||||
@portal_program ||= Program.find_by_redirect_uri_host(portal_return_url)
|
||||
end
|
||||
|
||||
def portal_onboarding_scenario
|
||||
return @portal_onboarding_scenario if defined?(@portal_onboarding_scenario)
|
||||
scenario_class = portal_program&.onboarding_scenario_class
|
||||
@portal_onboarding_scenario = scenario_class&.new(current_identity)
|
||||
end
|
||||
|
||||
def store_return_url
|
||||
session[:portal_return_to] = validated_return_url if params[:return_to].present?
|
||||
end
|
||||
|
|
|
|||
BIN
app/frontend/images/flavortown/bg-dark.png
Normal file
BIN
app/frontend/images/flavortown/bg-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
app/frontend/images/flavortown/hero-bg.webp
Normal file
BIN
app/frontend/images/flavortown/hero-bg.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
BIN
app/frontend/images/flavortown/logo.avif
Normal file
BIN
app/frontend/images/flavortown/logo.avif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
app/frontend/images/flavortown/onboarding(3).png
Normal file
BIN
app/frontend/images/flavortown/onboarding(3).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
|
|
@ -12,23 +12,43 @@
|
|||
@include dark-mode {
|
||||
background: #1a1d23;
|
||||
}
|
||||
|
||||
&.has-background {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
.brand-logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
object-fit: contain;
|
||||
|
||||
&--custom {
|
||||
border: 1px solid var(--surface-2-border);
|
||||
background: var(--surface-1);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
.brand-plus {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-strong);
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,48 @@
|
|||
.brand {
|
||||
&>h1 {
|
||||
display: inline;
|
||||
margin-left: 1rem;
|
||||
vertical-align: middle;
|
||||
font-size: 29px;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
}
|
||||
|
||||
& img {
|
||||
width: 50px;
|
||||
display: inline;
|
||||
}
|
||||
.portal-container {
|
||||
min-height: 100vh;
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 3rem;
|
||||
|
||||
> .brand {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.brand-logos {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
object-fit: contain;
|
||||
|
||||
&--custom {
|
||||
border: 1px solid var(--surface-2-border);
|
||||
background: var(--surface-1);
|
||||
}
|
||||
}
|
||||
|
||||
.brand-plus {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class OAuthScope
|
|||
consent_fields: [
|
||||
{ key: :address, value: ->(ident) {
|
||||
addr = ident.primary_address
|
||||
addr ? [addr.line_1, addr.city, addr.state, addr.country].compact.join(", ") : nil
|
||||
addr ? [ addr.line_1, addr.city, addr.state, addr.country ].compact.join(", ") : nil
|
||||
} }
|
||||
]
|
||||
),
|
||||
|
|
@ -102,7 +102,7 @@ class OAuthScope
|
|||
description: "See your legal name",
|
||||
icon: "card-id",
|
||||
consent_fields: [
|
||||
{ key: :legal_name, value: ->(ident) { [ident.legal_first_name, ident.legal_last_name].compact.join(" ").presence } }
|
||||
{ key: :legal_name, value: ->(ident) { [ ident.legal_first_name, ident.legal_last_name ].compact.join(" ").presence } }
|
||||
]
|
||||
),
|
||||
new(
|
||||
|
|
@ -191,7 +191,7 @@ class OAuthScope
|
|||
scope_names.flat_map do |name|
|
||||
scope = find(name)
|
||||
next [] unless scope
|
||||
[scope] + scope.includes.filter_map { |n| find(n) }
|
||||
[ scope ] + scope.includes.filter_map { |n| find(n) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -95,6 +95,12 @@ module OnboardingScenarios
|
|||
def bot_name = nil
|
||||
def bot_icon_url = nil
|
||||
|
||||
# Branding - override to customize logo/background for welcome, OAuth, and portal pages
|
||||
# logo_path: vite asset path like "images/flavortown.png"
|
||||
def logo_path = nil
|
||||
def background_path = nil
|
||||
def dark_mode_background_path = background_path
|
||||
|
||||
# Custom dialogue flow hooks - override in subclasses
|
||||
def before_first_message = nil
|
||||
def after_promotion = nil
|
||||
|
|
|
|||
|
|
@ -70,5 +70,9 @@ module OnboardingScenarios
|
|||
when "flavortown_dino_nuggets" then :dino_nuggets
|
||||
end
|
||||
end
|
||||
|
||||
def logo_path = "images/flavortown/logo.avif"
|
||||
def background_path = "images/flavortown/hero-bg.webp"
|
||||
def dark_mode_background_path = "images/flavortown/bg-dark.png"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -86,6 +86,38 @@ class Program < ApplicationRecord
|
|||
|
||||
def authorized_for_identity?(identity) = authorized_tokens.exists?(resource_owner: identity)
|
||||
|
||||
def onboarding_scenario_class
|
||||
return nil if onboarding_scenario.blank?
|
||||
OnboardingScenarios::Base.find_by_slug(onboarding_scenario)
|
||||
end
|
||||
|
||||
def onboarding_scenario_instance(identity = nil)
|
||||
onboarding_scenario_class&.new(identity)
|
||||
end
|
||||
|
||||
def self.find_by_redirect_uri_host(url)
|
||||
return nil if url.blank?
|
||||
begin
|
||||
uri = URI.parse(url)
|
||||
host = uri.host
|
||||
return nil unless host
|
||||
|
||||
find_each do |program|
|
||||
program.redirect_uri.to_s.split("\n").each do |redirect_uri|
|
||||
begin
|
||||
redirect_host = URI.parse(redirect_uri.strip).host
|
||||
return program if redirect_host == host
|
||||
rescue URI::InvalidURIError
|
||||
next
|
||||
end
|
||||
end
|
||||
end
|
||||
nil
|
||||
rescue URI::InvalidURIError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_community_scopes
|
||||
|
|
|
|||
|
|
@ -77,10 +77,8 @@
|
|||
|
||||
<section class="section-card add-address-card" x-data="{ showForm: false }">
|
||||
<template x-if="!showForm">
|
||||
<button @click="showForm = true" class="secondary add-address-btn">
|
||||
<span class="add-icon">+</span>
|
||||
<%= local_assigns[:portal] ? t("portal.addresses.manage.add_new") : t("addresses.add_new") %>
|
||||
</button>
|
||||
<button
|
||||
<%= local_assigns[:portal] ? t("portal.addresses.manage.add_new") : t("addresses.add_new") %>>
|
||||
</template>
|
||||
<template x-if="showForm">
|
||||
<div class="address-form" x-init="htmx.process($el)">
|
||||
|
|
|
|||
|
|
@ -1,12 +1,30 @@
|
|||
<div class="auth-container">
|
||||
<%
|
||||
application = Program.find_by(uid: @pre_auth.client.uid)
|
||||
scenario_class = application&.onboarding_scenario_class
|
||||
scenario = scenario_class&.new(current_identity)
|
||||
custom_logo_path = scenario&.logo_path
|
||||
light_bg = scenario&.background_path
|
||||
dark_bg = scenario&.dark_mode_background_path
|
||||
%>
|
||||
<% if light_bg || dark_bg %>
|
||||
<style>
|
||||
body { background-size: cover; background-position: center; }
|
||||
<% if light_bg %>html[data-theme="light"] body { background-image: url('<%= vite_asset_path(light_bg) %>'); }<% end %>
|
||||
<% if dark_bg %>html[data-theme="dark"] body { background-image: url('<%= vite_asset_path(dark_bg) %>'); }<% end %>
|
||||
</style>
|
||||
<% end %>
|
||||
<div class="auth-container<%= ' has-background' if light_bg || dark_bg %>">
|
||||
<div class="auth-brand">
|
||||
<%= vite_image_tag "images/hc-square.png", alt: "Hack Club logo", class: "brand-logo" %>
|
||||
<span><%= t("brand") %></span>
|
||||
<% if custom_logo_path %>
|
||||
<span class="brand-plus">×</span>
|
||||
<%= vite_image_tag custom_logo_path, alt: "Logo", class: "brand-logo brand-logo--custom" %>
|
||||
<% end %>
|
||||
<span class="brand-text"><%= t("brand") %></span>
|
||||
</div>
|
||||
<div class="auth-card oauth-consent">
|
||||
<header>
|
||||
<h1><%= session.dig(:stashed_data, "splash_message") || "Continue to #{@pre_auth.client.name}?" %></h1>
|
||||
<% application = Program.find_by(uid: @pre_auth.client.uid) %>
|
||||
<small>
|
||||
<%= raw t('.prompt', client_name: content_tag(:strong) { @pre_auth.client.name }) %>
|
||||
</small>
|
||||
|
|
|
|||
|
|
@ -23,10 +23,23 @@
|
|||
<%= vite_stylesheet_tag "application.css" %>
|
||||
<%= vite_javascript_tag 'application' %>
|
||||
</head>
|
||||
<body class="<%= content_for?(:portal_wrapper) ? '' : (content_for?(:body_class) ? yield(:body_class) : 'auth-layout') %>" hx-headers='{"X-CSRF-Token": "<%= form_authenticity_token %>"}' hx-boost="false">
|
||||
<%
|
||||
body_style = content_for?(:body_style) ? yield(:body_style) : nil
|
||||
portal_scenario = respond_to?(:portal_onboarding_scenario) ? portal_onboarding_scenario : nil
|
||||
light_bg = portal_scenario&.background_path
|
||||
dark_bg = portal_scenario&.dark_mode_background_path
|
||||
%>
|
||||
<body class="<%= content_for?(:portal_wrapper) ? '' : (content_for?(:body_class) ? yield(:body_class) : 'auth-layout') %>" hx-headers='{"X-CSRF-Token": "<%= form_authenticity_token %>"}' hx-boost="false"<%= " style=\"#{body_style}\"".html_safe if body_style %>>
|
||||
<% if content_for?(:portal_wrapper) && (light_bg || dark_bg) %>
|
||||
<style>
|
||||
body { background-size: cover; background-position: center; }
|
||||
<% if light_bg %>html[data-theme="light"] body { background-image: url('<%= vite_asset_path(light_bg) %>'); }<% end %>
|
||||
<% if dark_bg %>html[data-theme="dark"] body { background-image: url('<%= vite_asset_path(dark_bg) %>'); }<% end %>
|
||||
</style>
|
||||
<% end %>
|
||||
<% if content_for?(:portal_wrapper) %>
|
||||
<main class="container portal-container">
|
||||
<%= render Components::Brand.new(identity: current_identity) %>
|
||||
<%= render Components::Brand.new(identity: current_identity, logo_path: portal_scenario&.logo_path) %>
|
||||
<%= render "shared/flash" %>
|
||||
<span id="async_flash"></span>
|
||||
<%= yield %>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
<% content_for(:portal_wrapper, true) %>
|
||||
<div class="container-sm">
|
||||
<div class="page-sections">
|
||||
<article>
|
||||
<h1>hey! <%= emoji_image "hyper-dino-wave-flip.gif" %></h1>
|
||||
<p>We're excited to send you stuff!</p>
|
||||
<p>First, we need to verify you're under 18.</p>
|
||||
<p>In the past year, Hack Club has given out ~$1M in grants to students like you, and with that comes a lot of adults trying to slip in.</p>
|
||||
<p>This quick check helps us make sure our resources go to real teenage hackers.</p>
|
||||
<% if portal_onboarding_scenario&.logo_path %>
|
||||
<div style="text-align: center; margin-bottom: 2rem;">
|
||||
<%= vite_image_tag portal_onboarding_scenario.logo_path, alt: "Logo", style: "height: 150px;" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<article class="section-card" style="position: relative;">
|
||||
<h1>hey! <%= emoji_image "hyper-dino-wave-flip.gif" %></h1>
|
||||
<p>We're excited to send you stuff!</p>
|
||||
<p>First, we need to verify you're under 18.</p>
|
||||
<p>In the past year, Hack Club has given out ~$1M in grants to students like you, and with that comes a lot of adults trying to slip in.</p>
|
||||
<p>This quick check helps us make sure our resources go to real teenage hackers.</p>
|
||||
|
||||
<div class="status-actions">
|
||||
<%= link_to "alright! →", portal_verify_document_path, role: "button" %>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="status-actions">
|
||||
<%= link_to "alright! →", portal_verify_document_path, role: "button" %>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
<% service_name = @program&.name || "the application" %>
|
||||
<% scenario_class = @program&.onboarding_scenario_class %>
|
||||
<% scenario = scenario_class&.new(nil) %>
|
||||
<% if scenario&.background_url %>
|
||||
<% content_for(:body_style) { "background-image: url('#{scenario.background_url}'); background-size: cover; background-position: center;" } %>
|
||||
<% end %>
|
||||
<%= render Components::AuthWelcome.new(
|
||||
headline: "Continue to #{service_name}",
|
||||
subtitle: "Sign in or create an account to continue",
|
||||
return_to: @return_to,
|
||||
login_hint: @login_hint
|
||||
login_hint: @login_hint,
|
||||
logo_path: scenario&.logo_path
|
||||
) %>
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ module IdentityVault
|
|||
end
|
||||
|
||||
config.to_prepare do
|
||||
Doorkeeper::ApplicationController.layout "application"
|
||||
Doorkeeper::ApplicationController.layout "logged_out"
|
||||
Doorkeeper::ApplicationController.skip_before_action :authenticate_identity!
|
||||
Backend::NoAuthController.skip_after_action :verify_authorized
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue