1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-20 00:11:35 +02:00

(Daily sync) Password reset pages & client portal rework (#3492)

* Dependency clearing

* Tailwind & templates cleanup

* Password reset pages & more features:
- New $this->render() method
- Password reset pages
- Tailwind CSS scaffold
- New styles for buttons, inputs, alerts
- Changed to shorthand syntax for language file (en)
- Added app.css and app.js which will be main endpoint
- Added new 'theme' field inside of ninja.php
- Scaffold for 'ninja2020' theme: both client and global theme
- Ignoring local builds of assets, until purgeCSS is there
- Overall cleanup

* Switch back default template to 'default'

* Remove app.css build

* Fix Codacy

* Fix Codacy 'doublequote' issues
This commit is contained in:
Benjamin Beganović 2020-03-13 22:17:08 +01:00 committed by GitHub
parent 648cd73bec
commit aad9f81e93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 9820 additions and 108 deletions

4
.gitignore vendored
View File

@ -26,3 +26,7 @@ public/mix-manifest.json
# Ignore local migrations
storage/migrations
# Ignore Tailwind & Javascript build file >2mb without PurgeCSS (development-only)
public/css/app.css
public/js/app.js

View File

@ -90,7 +90,6 @@ class ForgotPasswordController extends Controller
* example="Unable to send password reset link",
* ),
* ),
* ),
* @OA\Response(
* response="default",
@ -110,9 +109,20 @@ class ForgotPasswordController extends Controller
$response = $this->broker()->sendResetLink(
$this->credentials($request)
);
if ($request->ajax()) {
return $response == Password::RESET_LINK_SENT
? response()->json(['message' => 'Reset link sent to your email.', 'status' => true], 201)
: response()->json(['message' => 'Email not found', 'status' => false], 401);
}
return $response == Password::RESET_LINK_SENT
? response()->json(['message' => 'Reset link sent to your email.', 'status' => true], 201)
: response()->json(['message' => 'Email not found', 'status' => false], 401);
? $this->sendResetLinkResponse($request, $response)
: $this->sendResetLinkFailedResponse($request, $response);
}
public function showLinkRequestForm()
{
return $this->render('auth.passwords.request', ['root' => 'themes']);
}
}

View File

@ -13,6 +13,8 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
class ResetPasswordController extends Controller
{
@ -34,7 +36,7 @@ class ResetPasswordController extends Controller
*
* @var string
*/
protected $redirectTo = '/dashboard';
protected $redirectTo = '/';
/**
* Create a new controller instance.
@ -45,4 +47,47 @@ class ResetPasswordController extends Controller
{
$this->middleware('guest');
}
public function showResetForm(Request $request, $token = null)
{
return $this->render('auth.passwords.reset', ['root' => 'themes', 'token' => $token]);
}
/**
* Reset the given user's password.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
public function reset(Request $request)
{
$request->validate($this->rules(), $this->validationErrorMessages());
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$response = $this->broker()->reset(
$this->credentials($request), function ($user, $password) {
$this->resetPassword($user, $password);
}
);
// Added this because it collides the session between
// client & main portal giving unlimited redirects.
auth()->logout();
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $response == Password::PASSWORD_RESET
? $this->sendResetResponse($request, $response)
: $this->sendResetFailedResponse($request, $response);
}
public function afterReset()
{
auth()->logout();
return redirect('/');
}
}

View File

@ -20,11 +20,11 @@ class DashboardController extends Controller
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function index()
{
return view('portal.default.dashboard.index');
return view('dashboard.index');
}
/**

View File

@ -20,4 +20,23 @@ use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
/**
* @param string $path
* @param array $options
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function render(string $path, array $options = [])
{
$theme = array_key_exists('theme', $options) ? $options['theme'] : 'ninja2020';
if (array_key_exists('root', $options)) {
return view(
sprintf('%s.%s.%s', $options['root'], $theme, $path)
, $options);
}
return view("portal.$theme.$path", $options);
}
}

View File

@ -128,5 +128,8 @@ return [
]
],
'themes' => [
'global' => 'ninja2020',
'portal' => 'ninja2020',
],
];

9443
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,28 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "npm run development -- --watch",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {},
"dependencies": {
"@coreui/coreui": "^2.1.12",
"@coreui/icons": "^0.3.0",
"@ttskch/select2-bootstrap4-theme": "^1.2.3",
"bootstrap": "^4.3.1",
"bootstrap-sweetalert": "^1.0.1",
"cross-env": "^5.2.0",
"dropzone": "^5.5.1",
"font-awesome": "^4.7.0",
"jquery": "^3.4.1",
"jsignature": "^2.1.3",
"laravel-mix": "^4.1.2",
"perfect-scrollbar": "^1.3.0",
"popper.js": "^1.14.3",
"puppeteer": "^1.20.0",
"select2": "^4.0.8"
}
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "npm run development -- --watch",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"vue-template-compiler": "^2.6.11"
},
"dependencies": {
"@tailwindcss/ui": "^0.1.3",
"axios": "^0.19",
"cross-env": "^7.0",
"jsignature": "^2.1.3",
"laravel-mix": "^5.0.1",
"lodash": "^4.17.13",
"puppeteer": "^1.20.0",
"resolve-url-loader": "^3.1.0",
"sass": "^1.15.2",
"sass-loader": "^8.0.0",
"tailwindcss": "^1.2.0"
}
}

6
public/css/app.css vendored

File diff suppressed because one or more lines are too long

1
resources/js/app.js vendored Normal file
View File

@ -0,0 +1 @@
// ..

View File

@ -1,7 +1,10 @@
<?php
$LANG = array(
return [
'continue' => 'Continue',
'complete' => 'Complete',
'next' => 'Next',
'next_step' => 'Next step',
'organization' => 'Organization',
'name' => 'Name',
'website' => 'Website',
@ -263,6 +266,8 @@ $LANG = array(
'notification_credit_viewed' => 'The following client :client viewed Credit :credit for :amount.',
'notification_quote_viewed' => 'The following client :client viewed Quote :quote for :amount.',
'reset_password' => 'You can reset your account password by clicking the following button:',
'reset_password_text' => 'Enter your email to reset your password.',
'password_reset' => 'Password reset',
'secure_payment' => 'Secure Payment',
'card_number' => 'Card Number',
'expiration_month' => 'Expiration Month',
@ -3135,8 +3140,4 @@ $LANG = array(
'invoice_number_placeholder' => 'Invoice # :invoice',
'entity_number_placeholder' => ':entity # :entity_number',
'email_link_not_working' => 'If button above isn\'t working for you, please click on the link',
);
return $LANG;
?>
];

View File

@ -1,20 +0,0 @@
// Body
$body-bg: #f8fafc;
// Typography
$font-family-sans-serif: "Nunito", sans-serif;
$font-size-base: 0.9rem;
$line-height-base: 1.6;
// Colors
$blue: #3490dc;
$indigo: #6574cd;
$purple: #9561e2;
$pink: #f66D9b;
$red: #e3342f;
$orange: #f6993f;
$yellow: #ffed4a;
$green: #38c172;
$teal: #4dc0b5;
$cyan: #6cb2eb;

View File

@ -1,14 +1,12 @@
@tailwind base;
// Fonts
@import url('https://fonts.googleapis.com/css?family=Nunito');
// ..
@tailwind components;
// Variables
@import 'variables';
@import 'components/buttons';
@import 'components/validation';
@import 'components/inputs';
@import 'components/alerts';
// Bootstrap
@import '~bootstrap/scss/bootstrap';
.navbar-laravel {
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
}
// ..
@tailwind utilities;

11
resources/sass/components/alerts.scss vendored Normal file
View File

@ -0,0 +1,11 @@
.alert {
@apply bg-gray-100 py-3 mt-4 px-4 text-sm border-l-2 mt-2 mb-1 bg-gray-100 border-gray-400;
}
.alert-success {
@apply border-green-500;
}
.alert-failure {
@apply border-red-500;
}

15
resources/sass/components/buttons.scss vendored Normal file
View File

@ -0,0 +1,15 @@
.button {
@apply rounded py-3 px-4;
}
.button-primary {
@apply bg-blue-500 text-white;
&:hover {
@apply bg-blue-600;
}
}
.button-block {
@apply block w-full;
}

11
resources/sass/components/inputs.scss vendored Normal file
View File

@ -0,0 +1,11 @@
.input {
@apply items-center border border-gray-300 rounded mt-2 w-full py-3 px-4;
&:focus {
@apply outline-none border-blue-500;
}
}
.input-label {
@apply text-sm text-gray-600;
}

View File

@ -0,0 +1,11 @@
.validation {
@apply border-l-2 mt-2 mb-1 px-3 bg-gray-100 py-1;
}
.validation-fail {
@apply border-red-500 text-gray-700 text-sm;
}
.validation-pass {
@apply border-green-500 text-gray-700 text-sm;
}

View File

@ -0,0 +1,7 @@
@extends('portal.ninja2020.layout.clean')
@section('body')
<div class="m-4">
<button class="button">Hello world</button>
</div>
@endsection

View File

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<!-- Source: https://github.com/invoiceninja/invoiceninja -->
<!-- Error: {{ session('error') }} -->
@if (config('services.analytics.tracking_id'))
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-122229484-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', '{{ config('services.analytics.tracking_id') }}', {'anonymize_ip': true});
function trackEvent(category, action) {
ga('send', 'event', category, action, this.src);
}
</script>
<script>
Vue.config.devtools = true;
</script>
@else
<script>
function gtag() {
}
</script>
@endif
<!-- Title -->
<title>@yield('meta_title', 'Invoice Ninja') | {{ config('app.name') }}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="@yield('meta_description')"/>
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- Scripts -->
<script src="{{ mix('js/app.js') }}" defer></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap" rel="stylesheet" type="text/css">
<!-- Styles -->
<link href="{{ mix('css/app.css') }}" rel="stylesheet">
{{-- <link href="{{ mix('favicon.png') }}" rel="shortcut icon" type="image/png"> --}}
<link rel="canonical" href="{{ config('ninja.site_url') }}/{{ request()->path() }}"/>
{{-- Feel free to push anything to header using @push('header') --}}
@stack('head')
</head>
<body class="antialiased">
@yield('body')
</body>
<footer>
@yield('footer')
@stack('footer')
</footer>
</html>

View File

@ -0,0 +1,37 @@
@extends('portal.ninja2020.layout.clean')
@section('meta_title', ctrans('texts.password_recovery'))
@section('body')
<div class="flex h-screen">
<div class="m-auto md:w-1/3 lg:w-1/5">
<div class="flex flex-col">
<h1 class="text-center text-3xl">{{ ctrans('texts.password_recovery') }}</h1>
<p class="text-center opacity-75">{{ ctrans('texts.reset_password_text') }}</p>
@if(session('status'))
<div class="alert alert-success mt-4">
{{ session('status') }}
</div>
@endif
<form action="{{ route('password.email') }}" method="post" class="mt-6">
@csrf
<div class="flex flex-col">
<label for="email" class="text-sm text-gray-600">{{ ctrans('texts.email_address') }}</label>
<input type="email" name="email" id="email"
class="input"
placeholder="user@example.com"
value="{{ old('email') }}"
autofocus>
@error('email')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="mt-5">
<button class="button button-primary button-block">{{ ctrans('texts.next_step') }}</button>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,58 @@
@extends('portal.ninja2020.layout.clean')
@section('meta_title', ctrans('texts.password_reset'))
@section('body')
<div class="flex h-screen">
<div class="m-auto md:w-1/3 lg:w-1/5">
<div class="flex flex-col">
<h1 class="text-center text-3xl">{{ ctrans('texts.password_reset') }}</h1>
@if(session('status'))
<div class="alert alert-success mt-2">
{{ session('status') }}
</div>
@endif
<form action="{{ route('password.update') }}" method="post" class="mt-6">
@csrf
<input type="hidden" name="token" value="{{ $token }}">
<div class="flex flex-col">
<label for="email" class="input-label">{{ ctrans('texts.email_address') }}</label>
<input type="email" name="email" id="email"
class="input"
value="{{ $email ?? old('email') }}"
autofocus>
@error('email')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="flex flex-col mt-4">
<label for="password" class="input-label">{{ ctrans('texts.password') }}</label>
<input type="password" name="password" id="password"
class="input"
autofocus>
@error('password')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="flex flex-col mt-4">
<label for="password" class="input-label">{{ ctrans('texts.password') }}</label>
<input type="password" name="password_confirmation" id="password_confirmation"
class="input"
autofocus>
@error('password_confirmation')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="mt-5">
<button class="button button-primary button-block">{{ ctrans('texts.complete') }}</button>
</div>
</form>
</div>
</div>
</div>
@endsection

13
tailwind.config.js vendored Normal file
View File

@ -0,0 +1,13 @@
const defaultTheme = require("tailwindcss/defaultTheme");
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ["Open Sans", ...defaultTheme.fontFamily.sans]
}
}
},
variants: {},
plugins: [require("@tailwindcss/ui")]
};

37
webpack.mix.js vendored
View File

@ -1,4 +1,5 @@
const mix = require('laravel-mix');
const mix = require("laravel-mix");
const tailwindcss = require("tailwindcss");
/*
|--------------------------------------------------------------------------
@ -11,32 +12,12 @@ const mix = require('laravel-mix');
|
*/
mix.copyDirectory('node_modules/@coreui/coreui/dist/css/coreui.min.css', 'public/vendors/css/coreui.min.css');
mix.copyDirectory('node_modules/@coreui/icons/css/coreui-icons.min.css', 'public/vendors/css/coreui-icons.min.css');
mix.copyDirectory('node_modules/@coreui/coreui/dist/css/bootstrap.min.css', 'public/vendors/css/bootstrap.min.css');
mix.copyDirectory('node_modules/font-awesome/css/font-awesome.min.css', 'public/vendors/css/font-awesome.min.css');
mix.copyDirectory('node_modules/@coreui/coreui/dist/js/coreui.min.js', 'public/vendors/js/coreui.min.js');
mix.copyDirectory('node_modules/bootstrap/dist/js/bootstrap.bundle.min.js', 'public/vendors/js/bootstrap.bundle.min.js');
mix.copyDirectory('node_modules/jquery/dist/jquery.min.js', 'public/vendors/js/jquery.min.js');
mix.copyDirectory('node_modules/perfect-scrollbar/dist/perfect-scrollbar.min.js', 'public/vendors/js/perfect-scrollbar.min.js');
mix.copyDirectory('node_modules/jsignature/libs/jSignature.min.js', 'public/vendors/js/jSignature.min.js');
mix.copyDirectory('node_modules/jsignature/libs/flashcanvas.min.js', 'public/vendors/js/flashcanvas.min.js');
mix.copyDirectory('node_modules/jsignature/libs/flashcanvas.swf', 'public/vendors/js/flashcanvas.swf');
mix.copyDirectory('node_modules/select2/dist/css/select2.min.css', 'public/vendors/css/select2.min.css');
mix.copyDirectory('node_modules/select2/dist/js/select2.full.min.js', 'public/vendors/js/select2.min.js');
mix.copyDirectory('node_modules/@ttskch/select2-bootstrap4-theme/dist/select2-bootstrap4.min.css', 'public/vendors/css/select2-bootstrap4.css');
mix.copyDirectory('node_modules/dropzone/dist/min/dropzone.min.css', 'public/vendors/css/dropzone.min.css');
mix.copyDirectory('node_modules/dropzone/dist/min/basic.min.css', 'public/vendors/css/dropzone-basic.min.css');
mix.copyDirectory('node_modules/dropzone/dist/min/dropzone.min.js', 'public/vendors/js/dropzone.min.js');
mix.copyDirectory('node_modules/bootstrap-sweetalert/dist/sweetalert.css', 'public/vendors/css/sweetalert.css');
mix.copyDirectory('node_modules/bootstrap-sweetalert/dist/sweetalert.min.js', 'public/vendors/js/sweetalert.min.js');
mix.copyDirectory('node_modules/font-awesome/fonts', 'public/vendors/fonts');
mix.js("resources/js/app.js", "public/js")
.sass("resources/sass/app.scss", "public/css")
.options({
processCssUrls: false,
postCss: [tailwindcss("./tailwind.config.js")]
});
mix.version();
mix.disableSuccessNotifications();