diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php index 89e999a8..950d9050 100644 --- a/src/app/Http/Controllers/API/SignupController.php +++ b/src/app/Http/Controllers/API/SignupController.php @@ -1,174 +1,260 @@ all(), [ - 'email' => 'required|email', + 'email' => 'required', 'name' => 'required', ] ); if ($v->fails()) { return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); } + // Validate user email (or phone) + if ($error = $this->validatePhoneOrEmail($request->email, $is_phone)) { + return response()->json(['status' => 'error', 'errors' => ['email' => $error]], 422); + } + // Generate the verification code $code = SignupCode::create([ 'data' => [ 'email' => $request->email, 'name' => $request->name, ] ]); - // TODO: send email/sms message + // Send email/sms message + $this->{$is_phone ? 'sendSMS' : 'sendEmail'}($code); return response()->json(['status' => 'success', 'code' => $code->code]); } /** * Validation of the verification code. * * @param Illuminate\Http\Request HTTP request * * @return \Illuminate\Http\JsonResponse JSON response */ public function verify(Request $request) { // Validate the request args $v = Validator::make( $request->all(), [ 'code' => 'required', 'short_code' => 'required', ] ); if ($v->fails()) { return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); } - // Validate the code + // Validate the verification code $code = SignupCode::find($request->code); if (empty($code) || $code->isExpired() || Str::upper($request->short_code) !== Str::upper($code->short_code) ) { $errors = ['short_code' => "The code is invalid or expired."]; return response()->json(['status' => 'error', 'errors' => $errors], 422); } // For signup last-step mode remember the code object, so we can delete it // with single SQL query (->delete()) instead of two (::destroy()) $this->code = $code; // Return user name and email/phone from the codes database on success return response()->json([ 'status' => 'success', 'email' => $code->data['email'], 'name' => $code->data['name'], ]); } /** * Finishes the signup process by creating the user account. * * @param Illuminate\Http\Request HTTP request * * @return \Illuminate\Http\JsonResponse JSON response */ public function signup(Request $request) { // Validate input $v = Validator::make( $request->all(), [ 'domain' => 'required|min:3', 'login' => 'required|min:2', 'password' => 'required|min:3|confirmed', ] ); if ($v->fails()) { return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); } $login = $request->login . '@' . $request->domain; - // TODO: check if specified domain is ours - // TODO: validate login + // Validate login (email) + if ($error = $this->validateEmail($login, true)) { + return response()->json(['status' => 'error', 'errors' => ['login' => $error]], 422); + } // Validate verification codes (again) $v = $this->verify($request); if ($v->status() !== 200) { return $v; } $code_data = $v->getData(); $user_name = $code_data->name; $user_email = $code_data->email; - // TODO: check if user with specified login already exists + // We allow only ASCII, so we can safely lower-case the email address + $login = Str::lower($login); $user = User::create( [ - // TODO: Save the external email (or phone) ? + // TODO: Save the external email (or phone) 'name' => $user_name, 'email' => $login, 'password' => $request->password, ] ); // Remove the verification code $this->code->delete(); $token = auth()->login($user); - return $this->respondWithToken($token); + return response()->json([ + 'access_token' => $token, + 'token_type' => 'bearer', + 'expires_in' => Auth::guard()->factory()->getTTL() * 60, + ]); } /** - * Get the token array structure. + * Checks if the input string is a valid email address or a phone number * - * @param string $token Respond with this token. + * @param string $email Email address or phone number + * @param bool &$is_phone Will be set to True if the string is valid phone number * - * @return \Illuminate\Http\JsonResponse JSON response + * @return string Error message on validation error */ - protected function respondWithToken($token) + protected function validatePhoneOrEmail($input, &$is_phone = false) { - return response()->json([ - 'access_token' => $token, - 'token_type' => 'bearer', - 'expires_in' => Auth::guard()->factory()->getTTL() * 60, - ]); + $is_phone = false; + + if (strpos($input, '@')) { + return $this->validateEmail($input); + } + + $input = str_replace(array('-', ' '), '', $input); + + if (!preg_match('/^\+?[0-9]{9,12}$/', $input)) { + return __('validation.noemailorphone'); + } + + $is_phone = true; + } + + /** + * Email address validation + * + * @param string $email Email address + * @param bool $signup Enables additional checks for signup mode + * + * @return string Error message on validation error + */ + protected function validateEmail($email, $signup = false) + { + $v = Validator::make(['email' => $email], ['email' => 'required|email']); + + if ($v->fails()) { + return __('validation.emailinvalid'); + } + + // Extended checks for an address that is supposed to become a login to Kolab + if ($signup) { + list($local, $domain) = explode('@', $email); + + // Local part validation + if (!preg_match('/^[A-Za-z0-9_.-]+$/', $local)) { + return __('validation.emailinvalid'); + } + + // Check if the local part is not one of exceptions + $exceptions = '/^(admin|administrator|sales|root)$/i'; + if (preg_match($exceptions, $local)) { + return __('validation.emailexists'); + } + + // Check if user with specified login already exists + if (User::where('email', $email)->first()) { + return __('validation.emailexists'); + } + + // TODO: check if specified domain is ours + } + } + + /** + * Sends SMS message for signup verification. + * + * @param App\SignupCode $code Verification code object + */ + protected function sendSMS(SignupCode $code) + { + // TODO + } + + /** + * Sends Email message for signup verification. + * + * @param App\SignupCode $code Verification code object + */ + protected function sendEmail(SignupCode $code) + { + // TODO } } diff --git a/src/resources/js/app.js b/src/resources/js/app.js index 5b505dc5..c47e60d7 100644 --- a/src/resources/js/app.js +++ b/src/resources/js/app.js @@ -1,82 +1,83 @@ /** * First we will load all of this project's JavaScript dependencies which * includes Vue and other libraries. It is a great starting point when * building robust, powerful web applications using Vue and Laravel. */ require('./bootstrap') window.Vue = require('vue') import router from '../vue/js/routes.js' import AppComponent from '../vue/components/App' const app = new Vue({ el: '#app', components: { AppComponent }, router, methods: { clearFormValidation: form => { $(form).find('.is-invalid').removeClass('is-invalid') $(form).find('.invalid-feedback').remove() } }, mounted() { this.$root.$on('clearFormValidation', (form) => { this.clearFormValidation(form) }) } }) import VueToastr from '@deveodk/vue-toastr' // You need a specific loader for CSS files like https://github.com/webpack/css-loader // If you would like custom styling of the toastr the css file can be replaced import '@deveodk/vue-toastr/dist/@deveodk/vue-toastr.css' Vue.use(VueToastr, { defaultPosition: 'toast-bottom-right', defaultTimeout: 50000 }) // Add a response interceptor for general/validation error handler window.axios.interceptors.response.use( response => { // Do nothing return response }, error => { var error_msg if (error.response && error.response.status == 422) { error_msg = "Form validation error" $.each(error.response.data.errors || {}, (idx, msg) => { $('form').each((i, form) => { const input_name = ($(form).data('validation-prefix') || '') + idx const input = $('#' + input_name) if (input.length) { input.addClass('is-invalid') - .parent().append($('