You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pixelfed/resources/views/auth/login.blade.php

435 lines
20 KiB
PHTML

@extends('layouts.app')
@section('content')
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="card shadow-none border">
<div class="card-header bg-transparent p-3 text-center">
<h4 class="font-weight-bold mb-0">
@if($step === '2fa')
{{ __('Two-factor authentication') }}
@elseif($step === 'verify')
{{ __('Verify your email') }}
@else
{{ __('auth.loginTitle') }}
@endif
</h4>
@if($returnTo)
<p class="small text-muted mb-0 mt-2">
{{ __('Continue to') }} <span class="font-weight-bold">{{ $returnTo }}</span>
</p>
@endif
</div>
@if (session('status'))
<div class="alert alert-success m-3">
<span class="font-weight-bold small"><i class="far fa-check-circle mr-2"></i> {{ session('status') }}</span>
</div>
@endif
@if($step === 'credentials')
@foreach ($errors->all() as $error)
<div class="alert alert-danger m-3">
<span class="font-weight-bold small"><i class="far fa-exclamation-triangle mr-2"></i> {{ $error }}</span>
</div>
@endforeach
@elseif($step === 'verify' && $errors->has('verify'))
<div class="alert alert-danger m-3">
<span class="font-weight-bold small"><i class="far fa-exclamation-triangle mr-2"></i> {{ $errors->first('verify') }}</span>
</div>
@endif
<div class="card-body">
@if($step === '2fa')
@php($backup = ($mode ?? 'totp') === 'backup')
<p class="text-center text-muted small mb-3">
@if($backup)
{{ __('Enter one of your backup codes.') }}
@else
{{ __('Enter the 6-digit code from your authenticator app.') }}
@endif
</p>
<form method="POST" action="{{ route('login.2fa') }}" id="twoFactorForm">
@csrf
<input type="hidden" name="mode" value="{{ $backup ? 'backup' : 'totp' }}">
<div class="form-group">
@if($backup)
<input
id="code"
type="text"
name="code"
data-mode="backup"
class="form-control form-control-lg text-center{{ $errors->has('code') ? ' is-invalid' : '' }}"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
maxlength="32"
placeholder="{{ __('Backup code') }}"
required
autofocus>
@else
<input
id="code"
type="text"
name="code"
data-mode="totp"
class="form-control form-control-lg text-center{{ $errors->has('code') ? ' is-invalid' : '' }}"
style="font-size:2rem;letter-spacing:.45em;text-indent:.45em;font-variant-numeric:tabular-nums"
inputmode="numeric"
autocomplete="one-time-code"
pattern="[0-9]*"
maxlength="6"
placeholder="000000"
required
autofocus>
@endif
@if ($errors->has('code'))
<span class="invalid-feedback d-block text-center">
<strong>{{ $errors->first('code') }}</strong>
</span>
@endif
</div>
<button type="submit" id="twoFactorSubmit" class="btn btn-primary btn-block btn-lg font-weight-bold rounded-pill">
{{ __('Verify') }}
</button>
</form>
<p class="text-center mt-3 mb-0">
@if($backup)
<a href="{{ route('login', ['step' => '2fa']) }}" class="small text-muted font-weight-bold">{{ __('Use your authenticator app instead') }}</a>
@else
<a href="{{ route('login', ['step' => '2fa', 'mode' => 'backup']) }}" class="small text-muted font-weight-bold">{{ __('Use a backup code instead') }}</a>
@endif
</p>
@elseif($step === 'verify')
@php($showChange = ($changeEmail ?? false) || $errors->has('email'))
<p class="text-center text-muted small mb-4">
{{ __('We sent a verification link to') }} <span class="font-weight-bold">{{ $pendingUser->email }}</span>. {{ __('Open it to finish signing in.') }}
</p>
<a href="{{ route('login.verify.continue') }}" class="btn btn-primary btn-block btn-lg font-weight-bold rounded-pill mb-2">
{{ __('I have verified my email') }}
</a>
<form method="POST" action="{{ route('login.verify.resend') }}">
@csrf
<button type="submit" class="btn btn-outline-primary btn-block font-weight-bold rounded-pill">
{{ __('Resend email') }}
</button>
</form>
@if($showChange)
<div class="mt-4">
<form method="POST" action="{{ route('login.verify.email') }}">
@csrf
<label for="newEmail" class="small font-weight-bold text-muted mb-0">{{ __('New email address') }}</label>
<div class="input-group">
<input
id="newEmail"
type="email"
name="email"
class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}"
value="{{ old('email') }}"
autocomplete="email"
required
autofocus>
<div class="input-group-append">
<button type="submit" class="btn btn-primary font-weight-bold">{{ __('Update') }}</button>
</div>
@if ($errors->has('email'))
<span class="invalid-feedback">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</div>
</form>
<p class="text-center mt-2 mb-0">
<a href="{{ route('login', ['step' => 'verify']) }}" class="small text-muted font-weight-bold">{{ __('Cancel') }}</a>
</p>
</div>
@else
<p class="text-center mt-3 mb-0">
<a href="{{ route('login', ['step' => 'verify', 'change' => 1]) }}" class="small text-muted font-weight-bold">{{ __('Wrong email address?') }}</a>
</p>
@endif
@else
<form method="POST" action="{{ route('login') }}">
@csrf
<div class="form-group row mb-3">
<div class="col-md-12">
<label for="email" class="small font-weight-bold text-muted mb-0">
{{ __('auth.emailAddress') }}
</label>
<input
id="email"
type="email"
name="email"
value="{{ old('email') }}"
class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}"
placeholder="{{ __('auth.email') }}"
autocomplete="username"
@if ($errors->has('email')) aria-invalid="true" aria-describedby="emailError" @endif
required
autofocus>
@if ($errors->has('email'))
<span id="emailError" class="invalid-feedback" role="alert">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row mb-2">
<div class="col-md-12">
<label for="password" class="small font-weight-bold text-muted mb-0">
{{ __('auth.password') }}
</label>
<div class="input-group">
<input
id="password"
type="password"
name="password"
class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}"
placeholder="{{ __('auth.password') }}"
autocomplete="current-password"
@if ($errors->has('password')) aria-invalid="true" aria-describedby="passwordError" @endif
required>
<div class="input-group-append">
<button
type="button"
id="togglePassword"
class="btn btn-outline-secondary"
aria-label="{{ __('Press and hold to show password') }}"
aria-controls="password"
aria-pressed="false">
<i class="far fa-eye" aria-hidden="true"></i>
</button>
</div>
</div>
@if ($errors->has('password'))
<span id="passwordError" class="invalid-feedback d-block" role="alert">
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-3">
<a href="{{ route('email.forgot') }}" class="small text-muted font-weight-bold">
{{ __('auth.forgotEmail') }}
</a>
<a href="{{ route('password.request') }}" class="small text-muted font-weight-bold">
{{ __('auth.forgot') }}
</a>
</div>
<div class="form-group row">
<div class="col-md-12">
<div class="custom-control custom-checkbox">
<input
id="remember"
type="checkbox"
name="remember"
class="custom-control-input"
{{ old('remember') ? 'checked' : '' }}>
<label class="custom-control-label font-weight-bold text-muted" for="remember">
{{ __('auth.remember') }}
</label>
</div>
</div>
</div>
<x-captcha surface="login" wrapperClass="d-flex justify-content-center mb-3" />
<button type="submit" class="btn btn-primary btn-block btn-lg font-weight-bold rounded-pill">
{{ __('auth.login') }}
</button>
</form>
@if(
(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled')) ||
(config('remote-auth.mastodon.ignore_closed_state') && config('remote-auth.mastodon.enabled'))
)
<hr>
<form method="POST" action="/auth/raw/mastodon/start">
@csrf
<div class="form-group row mb-0">
<div class="col-md-12">
<button type="submit" class="btn btn-primary btn-sm btn-block rounded-pill font-weight-bold" style="background: linear-gradient(#6364FF, #563ACC);">
{{ __('auth.signInMastodon') }}
</button>
</div>
</div>
</form>
@endif
@if( config('remote-auth.oidc.enabled') )
<hr>
<div class="form-group row mb-0">
<div class="col-md-12">
<a href="/auth/oidc/start" class="btn btn-primary btn-sm btn-block rounded-pill font-weight-bold" style="background: linear-gradient(#6364FF, #563ACC);">
Sign-in with OIDC
</a>
</div>
</div>
@endif
@if((bool) config_cache('pixelfed.open_registration') || (bool) config_cache('instance.curated_registration.enabled'))
<hr>
<p class="text-center font-weight-bold mb-0">
<a href="/register">{{ __('auth.register') }}</a>
</p>
@endif
@endif
@if($pendingUser)
<hr>
<div class="d-flex justify-content-between align-items-center">
<p class="mb-0 small text-muted">
{{ __('Signing in as') }} <span class="font-weight-bold">{{ '@'.$pendingUser->username }}</span>
</p>
<a href="{{ route('login.cancel') }}" class="small font-weight-bold">{{ __('Use a different account') }}</a>
</div>
@endif
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
(function() {
let submitting = false;
function lock() {
submitting = true;
const input = document.getElementById('code');
const btn = document.getElementById('twoFactorSubmit');
if (input) {
input.readOnly = true;
}
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span>';
}
}
document.addEventListener('input', function(e) {
if (e.target.id !== 'code' || e.target.dataset.mode !== 'totp') {
return;
}
const input = e.target;
input.value = input.value.replace(/\D/g, '').slice(0, 6);
if (input.value.length === 6 && !submitting && input.form) {
lock();
input.form.submit();
}
});
document.addEventListener('submit', function(e) {
if (e.target.id !== 'twoFactorForm') {
return;
}
if (submitting) {
e.preventDefault();
return;
}
lock();
});
document.addEventListener('DOMContentLoaded', function() {
const toggle = document.getElementById('togglePassword');
const password = document.getElementById('password');
if (toggle && password) {
const icon = toggle.querySelector('i');
const reveal = function() {
password.type = 'text';
toggle.setAttribute('aria-pressed', 'true');
if (icon) {
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
}
};
const conceal = function() {
password.type = 'password';
toggle.setAttribute('aria-pressed', 'false');
if (icon) {
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
};
toggle.addEventListener('mousedown', function(e) {
e.preventDefault();
reveal();
});
toggle.addEventListener('mouseup', conceal);
toggle.addEventListener('mouseleave', conceal);
// Touch devices.
toggle.addEventListener('touchstart', function(e) {
e.preventDefault();
reveal();
}, {
passive: false
});
toggle.addEventListener('touchend', conceal);
toggle.addEventListener('touchcancel', conceal);
toggle.addEventListener('keydown', function(e) {
if (e.key === ' ' || e.key === 'Enter' || e.key === 'Spacebar') {
e.preventDefault();
reveal();
}
});
toggle.addEventListener('keyup', function(e) {
if (e.key === ' ' || e.key === 'Enter' || e.key === 'Spacebar') {
conceal();
}
});
toggle.addEventListener('blur', conceal);
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
conceal();
}
});
}
const emailInput = document.getElementById('email');
if (!emailInput) {
return;
}
const email = new URLSearchParams(window.location.search).get('email');
if (email) {
emailInput.value = email;
const passwordInput = document.getElementById('password');
if (passwordInput) {
passwordInput.focus();
}
}
});
})();
</script>
@endpush