diff --git a/config/routes.php b/config/routes.php index bbd092e5..f0200db4 100644 --- a/config/routes.php +++ b/config/routes.php @@ -8,6 +8,8 @@ use FastRoute\RouteCollector; // Pages $route->get('/', 'HomeController@index'); +$route->get('/sign-up', 'SignUpController@view'); +$route->post('/sign-up', 'SignUpController@save'); $route->get('/credits', 'CreditsController@index'); $route->get('/health', 'HealthController@index'); diff --git a/db/factories/OAuthFactory.php b/db/factories/OAuthFactory.php new file mode 100644 index 00000000..0f3a203f --- /dev/null +++ b/db/factories/OAuthFactory.php @@ -0,0 +1,28 @@ + + */ + public function definition(): array + { + return [ + 'provider' => $this->faker->unique()->word(), + 'identifier' => $this->faker->unique()->word(), + 'access_token' => $this->faker->unique()->word(), + 'refresh_token' => $this->faker->unique()->word(), + 'expires_at' => '2099-12-31', + ]; + } +} diff --git a/includes/includes.php b/includes/includes.php index e16c4170..41f76c8a 100644 --- a/includes/includes.php +++ b/includes/includes.php @@ -62,7 +62,6 @@ $includeFiles = [ __DIR__ . '/../includes/pages/admin_groups.php', __DIR__ . '/../includes/pages/admin_shifts.php', __DIR__ . '/../includes/pages/admin_user.php', - __DIR__ . '/../includes/pages/guest_login.php', __DIR__ . '/../includes/pages/user_myshifts.php', __DIR__ . '/../includes/pages/user_shifts.php', diff --git a/includes/pages/guest_login.php b/includes/pages/guest_login.php deleted file mode 100644 index 851e7670..00000000 --- a/includes/pages/guest_login.php +++ /dev/null @@ -1,543 +0,0 @@ -user(); - $tshirt_sizes = config('tshirt_sizes'); - $goodie = GoodieType::from(config('goodie_type')); - $goodie_enabled = $goodie !== GoodieType::None; - $goodie_tshirt = $goodie === GoodieType::Tshirt; - $enable_user_name = config('enable_user_name'); - $enable_dect = config('enable_dect'); - $enable_planned_arrival = config('enable_planned_arrival'); - $min_password_length = config('min_password_length'); - $enable_password = config('enable_password'); - $enable_pronoun = config('enable_pronoun'); - $enable_mobile_show = config('enable_mobile_show'); - $config = config(); - $request = request(); - $session = session(); - /** @var Connection $db */ - $db = app(Database::class)->getConnection(); - $is_oauth = $session->has('oauth2_connect_provider'); - - $msg = ''; - $nick = ''; - $lastName = ''; - $preName = ''; - $dect = ''; - $mobile = ''; - $mobile_show = false; - $email = ''; - $pronoun = ''; - $email_shiftinfo = false; - $email_by_human_allowed = false; - $email_messages = false; - $email_news = false; - $email_goody = false; - $tshirt_size = ''; - $password_hash = ''; - $selected_angel_types = []; - $planned_arrival_date = null; - - /** @var AngelType[]|Collection $angel_types_source */ - $angel_types_source = AngelType::all(); - $angel_types = []; - if (!empty($session->get('oauth2_groups'))) { - /** @var OAuth2 $oauth */ - $oauth = app()->get(OAuth2::class); - $ssoTeams = $oauth->getSsoTeams($session->get('oauth2_connect_provider')); - foreach ($ssoTeams as $name => $team) { - if (in_array($name, $session->get('oauth2_groups'))) { - $selected_angel_types[] = $team['id']; - } - } - } - foreach ($angel_types_source as $angel_type) { - if ($angel_type->hide_register) { - continue; - } - $angel_types[$angel_type->id] = $angel_type->name - . ($angel_type->restricted ? ' (' . __('Requires introduction') . ')' : ''); - if (!$angel_type->restricted) { - $selected_angel_types[] = $angel_type->id; - } - } - - $oauth_enable_password = $session->get('oauth2_enable_password'); - if (!is_null($oauth_enable_password)) { - $enable_password = $oauth_enable_password; - } - - if ( - !auth()->can('register') // No registration permission - // Not authenticated and - || (!$authUser && !config('registration_enabled') && !$session->get('oauth2_allow_registration')) // Registration disabled - || (!$authUser && !$enable_password && !$is_oauth) // Password disabled and not oauth - ) { - error(__('Registration is disabled.')); - - return page_with_title(register_title(), [ - msg(), - ]); - } - - if ($request->hasPostData('submit')) { - $valid = true; - - if ($request->has('username')) { - $nick = trim($request->get('username')); - $nickValid = (new Username())->validate($nick); - - if (!$nickValid) { - $valid = false; - $msg .= error(sprintf( - __('Please enter a valid nick.') . ' ' . __('Use up to 24 letters, numbers or connecting punctuations for your nickname.'), - $nick - ), true); - } - if (User::whereName($nick)->count() > 0) { - $valid = false; - $msg .= error(sprintf(__('Your nick "%s" already exists.'), htmlspecialchars($nick)), true); - } - } else { - $valid = false; - $msg .= error(__('Please enter a nickname.'), true); - } - - if ($request->has('mobile_show') && $enable_mobile_show) { - $mobile_show = true; - } - - if ($request->has('email') && strlen(strip_request_item('email')) > 0) { - $email = strip_request_item('email'); - if (!check_email($email)) { - $valid = false; - $msg .= error(__('E-mail address is not correct.'), true); - } - if (User::whereEmail($email)->first()) { - $valid = false; - $msg .= error(__('E-mail address is already used by another user.'), true); - } - } else { - $valid = false; - $msg .= error(__('Please enter your e-mail.'), true); - } - - if ($request->has('email_shiftinfo')) { - $email_shiftinfo = true; - } - - if ($request->has('email_by_human_allowed')) { - $email_by_human_allowed = true; - } - - if ($request->has('email_messages')) { - $email_messages = true; - } - - if ($request->has('email_news')) { - $email_news = true; - } - - if ($request->has('email_goody')) { - $email_goody = true; - } - - if ($goodie_tshirt) { - if ($request->has('tshirt_size') && isset($tshirt_sizes[$request->input('tshirt_size')])) { - $tshirt_size = $request->input('tshirt_size'); - } else { - $valid = false; - $msg .= error(__('Please select your shirt size.'), true); - } - } - - if ($enable_password && $request->has('password') && strlen($request->postData('password')) >= $min_password_length) { - if ($request->postData('password') != $request->postData('password2')) { - $valid = false; - $msg .= error(__('Your passwords don\'t match.'), true); - } - } elseif ($enable_password) { - $valid = false; - $msg .= error(sprintf( - __('Your password is too short (please use at least %s characters).'), - $min_password_length - ), true); - } - - if ($request->has('planned_arrival_date') && $enable_planned_arrival) { - $tmp = parse_date('Y-m-d H:i', $request->input('planned_arrival_date') . ' 00:00'); - $result = User_validate_planned_arrival_date($tmp); - $planned_arrival_date = $result->getValue(); - if (!$result->isValid()) { - $valid = false; - error(__('Please enter your planned date of arrival. It should be after the buildup start date and before teardown end date.')); - } - } elseif ($enable_planned_arrival) { - $valid = false; - error(__('Please enter your planned date of arrival. It should be after the buildup start date and before teardown end date.')); - } - - $selected_angel_types = []; - foreach (array_keys($angel_types) as $angel_type_id) { - if ($request->has('angel_types_' . $angel_type_id)) { - $selected_angel_types[] = $angel_type_id; - } - } - - // Trivia - if ($enable_user_name && $request->has('lastname')) { - $lastName = strip_request_item('lastname'); - } - if ($enable_user_name && $request->has('prename')) { - $preName = strip_request_item('prename'); - } - if ($enable_pronoun && $request->has('pronoun')) { - $pronoun = strip_request_item('pronoun'); - } - if ($enable_dect && $request->has('dect')) { - if (strlen(strip_request_item('dect')) <= 40) { - $dect = strip_request_item('dect'); - } else { - $valid = false; - error(__('For dect numbers are only 40 digits allowed.')); - } - } - if ($request->has('mobile')) { - $mobile = strip_request_item('mobile'); - } - - if ($valid) { - // Safeguard against partially created user data - $db->beginTransaction(); - - $user = new User([ - 'name' => $nick, - 'password' => $password_hash, - 'email' => $email, - 'api_key' => '', - 'last_login_at' => null, - ]); - $user->save(); - - $contact = new Contact([ - 'dect' => $dect, - 'mobile' => $mobile, - ]); - $contact->user() - ->associate($user) - ->save(); - - $personalData = new PersonalData([ - 'first_name' => $preName, - 'last_name' => $lastName, - 'pronoun' => $pronoun, - 'shirt_size' => $tshirt_size, - 'planned_arrival_date' => $enable_planned_arrival ? Carbon::createFromTimestamp($planned_arrival_date) : null, - ]); - $personalData->user() - ->associate($user) - ->save(); - - $settings = new Settings([ - 'language' => $session->get('locale'), - 'theme' => config('theme'), - 'email_human' => $email_by_human_allowed, - 'email_messages' => $email_messages, - 'email_goody' => $email_goody, - 'email_shiftinfo' => $email_shiftinfo, - 'email_news' => $email_news, - 'mobile_show' => $mobile_show, - ]); - $settings->user() - ->associate($user) - ->save(); - - $state = new State([]); - if (config('autoarrive')) { - $state->arrived = true; - $state->arrival_date = new Carbon(); - } - $state->user() - ->associate($user) - ->save(); - - if ($session->has('oauth2_connect_provider') && $session->has('oauth2_user_id')) { - $oauth = new OAuth([ - 'provider' => $session->get('oauth2_connect_provider'), - 'identifier' => $session->get('oauth2_user_id'), - 'access_token' => $session->get('oauth2_access_token'), - 'refresh_token' => $session->get('oauth2_refresh_token'), - 'expires_at' => $session->get('oauth2_expires_at'), - ]); - $oauth->user() - ->associate($user) - ->save(); - - $session->remove('oauth2_connect_provider'); - $session->remove('oauth2_user_id'); - $session->remove('oauth2_access_token'); - $session->remove('oauth2_refresh_token'); - $session->remove('oauth2_expires_at'); - } - - // Assign user-group and set password - $defaultGroup = Group::find(auth()->getDefaultRole()); - $user->groups()->attach($defaultGroup); - if ($enable_password) { - auth()->setPassword($user, $request->postData('password')); - } - - // Assign angel-types - $user_angel_types_info = []; - foreach ($selected_angel_types as $selected_angel_type_id) { - $angelType = AngelType::findOrFail($selected_angel_type_id); - $user->userAngelTypes()->attach($angelType); - $user_angel_types_info[] = $angelType->name; - } - - // Commit complete user data - $db->commit(); - - engelsystem_log( - 'User ' . User_Nick_render($user, true) - . ' signed up as: ' . join(', ', $user_angel_types_info) - ); - success(__('Angel registration successful!')); - - // User is already logged in - that means a supporter has registered an angel. Return to register page. - if ($authUser) { - throw_redirect(page_link_to('register')); - } - - // If a welcome message is present, display it on the next page - if ($config->get('welcome_msg')) { - $session->set('show_welcome', true); - } - - // Login the user - if ($user->oauth->count()) { - /** @var OAuth $provider */ - $provider = $user->oauth->first(); - throw_redirect(url('/oauth/' . $provider->provider)); - } - - throw_redirect(page_link_to('/')); - } - } - - $buildup_start_date = time(); - $teardown_end_date = null; - if ($buildup = $config->get('buildup_start')) { - /** @var Carbon $buildup */ - $buildup_start_date = $buildup->getTimestamp(); - } - - if ($teardown = $config->get('teardown_end')) { - /** @var Carbon $teardown */ - $teardown_end_date = $teardown->getTimestamp(); - } - - $form_data = $session->get('form_data'); - $session->remove('form_data'); - if (!$nick && !empty($form_data['name'])) { - $nick = $form_data['name']; - } - - if (!$email && !empty($form_data['email'])) { - $email = $form_data['email']; - } - - if (!$preName && !empty($form_data['first_name'])) { - $preName = $form_data['first_name']; - } - - if (!$lastName && !empty($form_data['last_name'])) { - $lastName = $form_data['last_name']; - } - - return page_with_title(register_title(), [ - __('By completing this form you\'re registering as a Chaos-Angel. This script will create you an account in the angel task scheduler.'), - form_info(entry_required() . ' = ' . __('Entry required!')), - $msg, - msg(), - form([ - div('row', [ - div('col', [ - form_text( - 'username', - __('Nick') . ' ' . entry_required(), - $nick, - false, - 24, - 'nickname' - ), - form_info( - '', - __('Use up to 24 letters, numbers or connecting punctuations for your nickname.') - ), - ]), - - $enable_pronoun ? div('col', [ - form_text('pronoun', __('Pronoun'), $pronoun, false, 15), - ]) : '', - ]), - - $enable_user_name ? div('row', [ - div('col', [ - form_text('prename', __('First name'), $preName, false, 64, 'given-name'), - ]), - div('col', [ - form_text('lastname', __('Last name'), $lastName, false, 64, 'family-name'), - ]), - ]) : '', - - div('row', [ - div('col', [ - form_email( - 'email', - __('E-Mail') . ' ' . entry_required(), - $email, - false, - 'email', - 254 - ), - form_checkbox( - 'email_shiftinfo', - __( - 'settings.profile.email_shiftinfo', - [config('app_name')] - ), - $email_shiftinfo - ), - form_checkbox( - 'email_news', - __('Notify me of new news'), - $email_news - ), - form_checkbox( - 'email_messages', - __('settings.profile.email_messages'), - $email_messages - ), - form_checkbox( - 'email_by_human_allowed', - __('Allow heaven angels to contact you by e-mail.'), - $email_by_human_allowed - ), - $goodie_enabled ? - form_checkbox( - 'email_goody', - __('To receive vouchers, give consent that nick, email address, worked hours and shirt size will be stored until the next similar event.') - . (config('privacy_email') ? ' ' . __('To withdraw your approval, send an email to %1$s.', [config('privacy_email')]) : ''), - $email_goody - ) : '', - ]), - - $enable_dect ? div('col', [ - form_text('dect', __('DECT'), $dect, false, 40, 'tel-local'), - ]) : '', - - div('col', [ - form_text('mobile', __('Mobile'), $mobile, false, 40, 'tel-national'), - $enable_mobile_show ? form_checkbox( - 'mobile_show', - __('Show mobile number to other users to contact me'), - $mobile_show - ) : '', - ]), - ]), - - div('row', [ - $enable_password ? div('col', [ - form_password('password', __('Password') . ' ' . entry_required(), 'new-password'), - ]) : '', - - $enable_planned_arrival ? div('col', [ - form_date( - 'planned_arrival_date', - __('Planned date of arrival') . ' ' . entry_required(), - $planned_arrival_date, - $buildup_start_date, - $teardown_end_date - ), - ]) : '', - ]), - - div('row', [ - $enable_password ? div('col', [ - form_password('password2', __('Confirm password') . ' ' . entry_required(), 'new-password'), - ]) : '', - - div('col', [ - $goodie_tshirt ? form_select( - 'tshirt_size', - __('Shirt size') . ' ' . entry_required(), - $tshirt_sizes, - $tshirt_size, - __('form.select_placeholder') - ) : '', - ]), - ]), - - div('row', [ - div('col', [ - form_checkboxes( - 'angel_types', - __('What do you want to do?') . sprintf( - ' (%s)', - url('/angeltypes/about'), - __('Description of job types') - ), - $angel_types, - $selected_angel_types - ), - form_info( - '', - __('Some angel types have to be confirmed later by a supporter at an introduction meeting. You can change your selection in the options section.') - ), - ]), - ]), - - form_submit('submit', __('Register')), - ]), - ]); -} - -/** - * @return string - */ -function entry_required() -{ - return icon('exclamation-triangle', 'text-info'); -} diff --git a/resources/assets/themes/cyborg_styles.scss b/resources/assets/themes/cyborg_styles.scss index a81b1d27..ad56fdb9 100644 --- a/resources/assets/themes/cyborg_styles.scss +++ b/resources/assets/themes/cyborg_styles.scss @@ -107,6 +107,10 @@ table, } } +.list-group .form-check-input { + border-color: $list-group-form-check-input-border-color; +} + // Navs ======================================================================= .nav-tabs, diff --git a/resources/assets/themes/cyborg_variables.scss b/resources/assets/themes/cyborg_variables.scss index db910a27..82216630 100644 --- a/resources/assets/themes/cyborg_variables.scss +++ b/resources/assets/themes/cyborg_variables.scss @@ -150,6 +150,9 @@ $input-border-focus: #66afe9 !default; //** Placeholder text color $input-color-placeholder: $gray-light !default; +$form-check-input-border: 1px solid $input-border-color !default; +$list-group-form-check-input-border-color: $input-border-color !default; + $legend-color: $text-color !default; $legend-border-color: $gray-dark !default; @@ -513,10 +516,11 @@ $progress-bar-info-bg: $info !default; // //## +$list-group-color: $input-color !default; //** Background color on `.list-group-item` $list-group-bg: $gray-darker !default; //** `.list-group-item` border color -$list-group-border: $gray-dark !default; +$list-group-border-color: $input-border-color !default; //** List group border radius $list-group-border-radius: $border-radius-base !default; diff --git a/resources/assets/themes/dark.scss b/resources/assets/themes/dark.scss index cf2d624a..9f892ee6 100644 --- a/resources/assets/themes/dark.scss +++ b/resources/assets/themes/dark.scss @@ -4,6 +4,7 @@ $input-disabled-bg: #111; $alert-bg-scale: 70%; $secondary: #222; $table-striped-bg: rgba(#fff, 0.05); +$list-group-form-check-input-border-color: #999; $es-choices-highlight-color: #000; diff --git a/resources/lang/de_DE/additional.po b/resources/lang/de_DE/additional.po index 1ecebf9f..cc82237d 100644 --- a/resources/lang/de_DE/additional.po +++ b/resources/lang/de_DE/additional.po @@ -17,19 +17,45 @@ msgstr "" msgid "validation.password.required" msgstr "Bitte gib ein Passwort an." +msgid "validation.password.length" +msgstr "Das angegebene Passwort ist zu kurz." + msgid "validation.login.required" msgstr "Bitte gib einen Loginnamen an." +msgid "validation.pronoun.optional" +msgstr "Das Pronomen, das Du eingegeben hast, ist zu lang. Verwende maximal 15 Zeichen." + +msgid "validation.firstname.optional" +msgstr "Der von dir eingegebene Vorname ist zu lang. Verwende maximal 64 Zeichen." + +msgid "validation.lastname.optional" +msgstr "Der von dir eingegebene Vorname ist zu lang. Verwende maximal 64 Zeichen." + +msgid "validation.mobile.optional" +msgstr "Der von dir eingegebene Handynummer ist zu lang. Verwende maximal 40 Zeichen." + +msgid "validation.dect.optional" +msgstr "Der von dir eingegebene DECT-Nummer ist zu lang. Verwende maximal 40 Zeichen." + +msgid "validation.username.required" +msgstr "Bitte gebe deinen Nick an." + +msgid "validation.username.username" +msgstr "" +"Bitte gebe einen gültigen Nick ein: " +"Verwende bis zu 24 Buchstaben, Zahlen oder verbindende Schriftzeichen (.-_) für deinen Nick." + msgid "validation.email.required" msgstr "Bitte gib eine E-Mail-Adresse an." msgid "validation.email.email" msgstr "Die E-Mail-Adresse ist nicht gültig." -msgid "validation.password.min" +msgid "validation.password.length" msgstr "Dein neues Passwort ist zu kurz." -msgid "validation.new_password.min" +msgid "validation.new_password.length" msgstr "Dein neues Passwort ist zu kurz." msgid "validation.password.confirmed" @@ -38,6 +64,21 @@ msgstr "Deine Passwörter stimmen nicht überein." msgid "validation.password_confirmation.required" msgstr "Du musst dein Passwort bestätigen." +msgid "validation.tshirt_size.required" +msgstr "Bitte wähle deine T-Shirt-Größe aus." + +msgid "validation.tshirt_size.shirtSize" +msgstr "Bitte wähle eine gültige T-Shirt-Größe aus." + +msgid "validation.planned_arrival_date.required" +msgstr "Bitte gebe dein geplantes Ankunftsdatum an." + +msgid "validation.planned_arrival_date.min" +msgstr "Das geplante Ankunftsdatum darf nicht vor Aufbaubeginn liegen." + +msgid "validation.planned_arrival_date.between" +msgstr "Das geplante Ankunftsdatum muss zwischen Aufbaubeginn und Abbauende liegen." + msgid "schedule.edit.success" msgstr "Das Programm wurde erfolgreich konfiguriert." diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po index d201f784..53c8995f 100644 --- a/resources/lang/de_DE/default.po +++ b/resources/lang/de_DE/default.po @@ -1020,15 +1020,6 @@ msgstr "" msgid "Edit user" msgstr "User bearbeiten" -msgid "Please enter a valid nick." -msgstr "Gib bitte einen erlaubten Nick an." - -msgid "Use up to 24 letters, numbers or connecting punctuations for your nickname." -msgstr "Verwende bis zu 24 Buchstaben, Zahlen oder verbindende Schriftzeichen (.-_) für deinen Nick." - -msgid "Your nick \"%s\" already exists." -msgstr "Der Nick \"%s\" existiert bereits." - msgid "Please enter a nickname." msgstr "Gib bitte einen Nick an." @@ -1067,9 +1058,6 @@ msgstr "" "Mit diesem Formular registrierst Du Dich als Engel. Du bekommst ein Konto in " "der Engel-Aufgabenverwaltung." -msgid "Notify me of new news" -msgstr "Benachrichtige mich bei neuen News" - msgid "Allow heaven angels to contact you by e-mail." msgstr "Erlaube Himmel-Engeln dich per Mail zu kontaktieren." @@ -1082,25 +1070,6 @@ msgstr "Um Voucher zu erhalten, stimme zu, dass Nick, E-Mail-Adresse, geleistete msgid "To withdraw your approval, send an email to %1$s." msgstr "Dies kann jederzeit durch eine E-Mail an %1$s widerrufen werden." -msgid "Planned date of arrival" -msgstr "Geplanter Ankunftstag" - -msgid "Shirt size" -msgstr "T-Shirt Größe" - -msgid "What do you want to do?" -msgstr "Was möchtest Du machen?" - -msgid "Description of job types" -msgstr "Beschreibung der Aufgaben" - -msgid "" -"Some angel types have to be confirmed later by a supporter at an " -"introduction meeting. You can change your selection in the options section." -msgstr "" -"Engeltypen welche eine Einführung benötigen, werden bei einem Einführungstreffen von " -"einem Supporter freigeschaltet. Du kannst Deine Auswahl später in den Einstellungen ändern." - msgid "Mobile" msgstr "Handy" @@ -2134,6 +2103,9 @@ msgstr "Pflichtfeld" msgid "settings.profile.nick" msgstr "Nick" +msgid "settings.profile.nick.already-taken" +msgstr "Der Nick ist bereits vergeben." + msgid "settings.profile.pronoun" msgstr "Pronomen" @@ -2161,9 +2133,15 @@ msgstr "Handy" msgid "settings.profile.mobile_show" msgstr "Mache meine Handynummer für andere Benutzer sichtbar." +msgid "settings.profile.email-preferences" +msgstr "E-Mail Einstellungen" + msgid "settings.profile.email" msgstr "E-Mail" +msgid "settings.profile.email.already-taken" +msgstr "Die E-Mail-Adresse ist bereits vergeben." + msgid "settings.profile.email_shiftinfo" msgstr "Das %s darf mir E-Mails senden (z.B. wenn sich meine Schichten ändern)." @@ -2195,6 +2173,9 @@ msgstr "Passwort" msgid "settings.password.info" msgstr "Hier kannst Du Dein Passwort ändern." +msgid "settings.password.confirmation-does-not-match" +msgstr "Passwort und Passwortwiederholung stimmen nicht überein." + msgid "settings.password.password" msgstr "Altes Passwort" @@ -2412,6 +2393,9 @@ msgid "angeltypes.restricted.hint" msgstr "Dieser Engeltyp benötigt eine Einweisung bei einem Einführungstreffen. " "Weitere Informationen findest du möglicherweise in der Beschreibung." +msgid "angeltypes.can-change-later" +msgstr "Du kannst Deine Auswahl später in den Einstellungen ändern." + msgid "angeltypes.name" msgstr "Name" @@ -2456,3 +2440,27 @@ msgstr "Tag %1$d" msgid "dashboard.day" msgstr "Tag" + +msgid "page.sign-up.title" +msgstr "Engelregistrierung" + +msgid "page.sign-up.login-data" +msgstr "Anmeldedaten" + +msgid "page.sign-up.name" +msgstr "Name" + +msgid "page.sign-up.event-data" +msgstr "Eventdaten" + +msgid "page.sign-up.what-do-you-want-to-do" +msgstr "Was möchtest Du machen?" + +msgid "page.sign-up.sign-up" +msgstr "Registrieren" + +msgid "pages.sign-up.disabled" +msgstr "Die Engelregistrierung ist deaktiviert" + +msgid "pages.sign-up.successful" +msgstr "Engelregistrierung erfolgreich! Du kannst dich jetzt anmelden!" diff --git a/resources/lang/en_US/additional.po b/resources/lang/en_US/additional.po index 52adf89c..7ebd9b24 100644 --- a/resources/lang/en_US/additional.po +++ b/resources/lang/en_US/additional.po @@ -15,19 +15,45 @@ msgstr "No user was found or password is wrong. Please try again. If you are sti msgid "validation.password.required" msgstr "The password is required." +msgid "validation.password.length" +msgstr "The password entered is too short." + msgid "validation.login.required" msgstr "The login name is required." +msgid "validation.pronoun.length" +msgstr "The pronoun you have entered is too long. Use a maximum of 15 characters." + +msgid "validation.firstname.length" +msgstr "The first name you have entered is too long. Use a maximum of 64 characters." + +msgid "validation.lastname.length" +msgstr "The last name you have entered is too long. Use a maximum of 64 characters." + +msgid "validation.mobile.optional" +msgstr "The mobile number you have entered is too long. Use a maximum of 40 characters." + +msgid "validation.dect.optional" +msgstr "The DECT number you have entered is too long. Use a maximum of 40 characters." + +msgid "validation.username.required" +msgstr "Please enter your nick." + +msgid "validation.username.username" +msgstr "" +"Please enter a valid nick:" +"Use up to 24 letters, numbers or connecting punctuations (.-_) for your nickname." + msgid "validation.email.required" msgstr "The email address is required." msgid "validation.email.email" msgstr "This email address is not valid." -msgid "validation.password.min" +msgid "validation.password.length" msgstr "Your new password is too short." -msgid "validation.new_password.min" +msgid "validation.new_password.length" msgstr "Your new password is too short." msgid "validation.password.confirmed" @@ -36,6 +62,21 @@ msgstr "Your passwords are not equal." msgid "validation.password_confirmation.required" msgstr "You have to confirm your password." +msgid "validation.tshirt_size.required" +msgstr "Please choose your t-shirt size." + +msgid "validation.tshirt_size.shirtSize" +msgstr "Please choose a valid t-shirt size." + +msgid "validation.planned_arrival_date.required" +msgstr "Please enter your planned date of arrival." + +msgid "validation.planned_arrival_date.min" +msgstr "The planned date of arrival must not be before the buil-up start date." + +msgid "validation.planned_arrival_date.between" +msgstr "The planned date of arrival must be between the build-up and tear-down date." + msgid "schedule.edit.success" msgstr "The schedule was configured successfully." diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po index e77731c8..e96eb415 100644 --- a/resources/lang/en_US/default.po +++ b/resources/lang/en_US/default.po @@ -229,6 +229,9 @@ msgstr "Entry required" msgid "settings.profile.nick" msgstr "Nick" +msgid "settings.profile.nick.already-taken" +msgstr "The nick is already taken." + msgid "settings.profile.pronoun" msgstr "Pronoun" @@ -256,9 +259,15 @@ msgstr "Mobile" msgid "settings.profile.mobile_show" msgstr "Show mobile number to other users to contact me." +msgid "settings.profile.email-preferences" +msgstr "E-Mail preferences" + msgid "settings.profile.email" msgstr "E-Mail" +msgid "settings.profile.email.already-taken" +msgstr "The email is already taken." + msgid "settings.profile.email_shiftinfo" msgstr "The %s is allowed to send me an e-mail (e.g. when my shifts change)." @@ -290,6 +299,9 @@ msgstr "Password" msgid "settings.password.info" msgstr "Here you can change your password." +msgid "settings.password.confirmation-does-not-match" +msgstr "Password and password confirmation do not match." + msgid "settings.password.password" msgstr "Old password" @@ -507,6 +519,9 @@ msgid "angeltypes.restricted.hint" msgstr "This angeltype requires the attendance at an introduction meeting. " "You might find additional information in the description." +msgid "angeltypes.can-change-later" +msgstr "You can change your selection later in the settings." + msgid "angeltypes.name" msgstr "Name" @@ -551,3 +566,27 @@ msgstr "Day %1$d" msgid "dashboard.day" msgstr "Day" + +msgid "page.sign-up.title" +msgstr "Angel sign-up" + +msgid "page.sign-up.login-data" +msgstr "Login data" + +msgid "page.sign-up.name" +msgstr "Name" + +msgid "page.sign-up.event-data" +msgstr "Event data" + +msgid "page.sign-up.what-do-you-want-to-do" +msgstr "What do you want to do?" + +msgid "page.sign-up.sign-up" +msgstr "Sign up" + +msgid "pages.sign-up.disabled" +msgstr "The angel registration is disabled" + +msgid "pages.sign-up.successful" +msgstr "Angel sign-up success. You can now log in." diff --git a/resources/views/layouts/parts/navbar.twig b/resources/views/layouts/parts/navbar.twig index bb84230a..cd072b3b 100644 --- a/resources/views/layouts/parts/navbar.twig +++ b/resources/views/layouts/parts/navbar.twig @@ -59,7 +59,7 @@ {% include "layouts/parts/language_dropdown.twig" %} {% if has_permission_to('register') and config('registration_enabled') %} - {{ _self.toolbar_item(__('Register'), url('register'), 'register', 'plus') }} + {{ _self.toolbar_item(__('Register'), url('/sign-up'), 'register', 'plus') }} {% endif %} {% if has_permission_to('login') %} diff --git a/resources/views/macros/form.twig b/resources/views/macros/form.twig index 9a98aa9f..f13ca267 100644 --- a/resources/views/macros/form.twig +++ b/resources/views/macros/form.twig @@ -214,9 +214,10 @@ Renders a Bootstrap checkbox element with mb-3. @param {bool} [opt.disabled=false] - Whether to add the "disabled" attribute. Defaults to false. @param {bool} [opt.raw_label=false] - Whether to use the raw label value (=do not escape). Defaults to false. @param {string} [opt.info] - If set an additional info icon will be added to the label with the text as tooltip. +@param {string} [opt.class="mb-3"] - CSS classes for the checkbox element. Defaults to "mb-3". #} {% macro checkbox(name, label, opt) %} -
+
{% else %} {% if has_permission_to('register') and config('registration_enabled') %} - + {{ __('registration.register') }} {% endif %} diff --git a/resources/views/pages/login.twig b/resources/views/pages/login.twig index 27400ffc..4373ba1f 100644 --- a/resources/views/pages/login.twig +++ b/resources/views/pages/login.twig @@ -106,7 +106,7 @@ {% if has_permission_to('register') and config('registration_enabled') %} {% if config('enable_password') %}

{{ __('Please sign up, if you want to help us!') }}

- {{ __('Register') }} » + {{ __('Register') }} » {% else %}

{{ __('Registration is only available via external login.') }}

{% endif %} diff --git a/resources/views/pages/settings/profile.twig b/resources/views/pages/settings/profile.twig index e8c90302..cb2d2470 100644 --- a/resources/views/pages/settings/profile.twig +++ b/resources/views/pages/settings/profile.twig @@ -109,6 +109,7 @@
+ {{ f.checkbox('email_shiftinfo', __('settings.profile.email_shiftinfo', [config('app_name')]), { 'checked': user.settings.email_shiftinfo, }) }} diff --git a/resources/views/pages/sign-up.twig b/resources/views/pages/sign-up.twig new file mode 100644 index 00000000..840234de --- /dev/null +++ b/resources/views/pages/sign-up.twig @@ -0,0 +1,310 @@ +{% extends 'layouts/app.twig' %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block title %}{{ __('page.sign-up.title') }}{% endblock %} + +{% block content %} +
+
+

{{ __('page.sign-up.title') }}

+
+ + {% include 'layouts/parts/messages.twig' %} + +
+ {{ csrf() }} + +
+

{{ __('page.sign-up.login-data') }}

+
+ {% if isPronounEnabled %} +
+ {{ f.input( + 'pronoun', + __('settings.profile.pronoun'), + { + 'max_length': 15, + 'value': f.formData('pronoun', ''), + } + ) }} +
+ {% endif %} + +
+ {{ f.input( + 'username', + __('settings.profile.nick'), + { + 'autocomplete': 'nickname', + 'max_length': 24, + 'required': true, + 'required_icon': true, + 'value': f.formData('username', ''), + } + ) }} +
+ + {% if not isPronounEnabled %} + {# Insert an empty row to keep password / email in line #} +
+ {% endif %} + + {% if isPasswordEnabled %} +
+ {{ f.input( + 'password', + __('settings.password'), + { + 'type': 'password', + 'autocomplete': 'new-password', + 'required': true, + 'required_icon': true, + } + ) }} +
+
+ {{ f.input( + "password_confirmation", + __('settings.password.new_password2'), + { + 'type': 'password', + 'autocomplete': 'new-password', + 'required': true, + 'required_icon': true, + } + ) }} +
+ {% endif %} + +
+ {{ f.input( + 'email', + __('settings.profile.email'), + { + 'type': 'email', + 'max_length': 254, + 'required': true, + 'required_icon': true, + 'value': f.formData('email', ''), + } + ) }} +
+
+ +
    +
  • + {# Empty class to prevent the default bottom margin #} + {{ f.checkbox( + 'email_shiftinfo', + __( + 'settings.profile.email_shiftinfo', + [config('app_name')] + ), + { + 'class': '', + 'checked': f.formData('email_shiftinfo', false), + }, + ) }} +
  • +
  • + {# Empty class to prevent the default bottom margin #} + {{ f.checkbox( + 'email_news', + __('settings.profile.email_news'), + { + 'class': '', + 'checked': f.formData('email_news', false), + }, + ) }} +
  • +
  • + {# Empty class to prevent the default bottom margin #} + {{ f.checkbox( + 'email_messages', + __('settings.profile.email_messages'), + { + 'class': '', + 'checked': f.formData('email_messages', false), + }, + ) }} +
  • +
  • + {# Empty class to prevent the default bottom margin #} + {{ f.checkbox( + 'email_by_human_allowed', + __('settings.profile.email_by_human_allowed'), + { + 'class': '', + 'checked': f.formData('email_by_human_allowed', false), + }, + ) }} +
  • +
+
+
+
+ + {% if isFullNameEnabled %} +
+

{{ __('page.sign-up.name') }}

+
+
+ {{ f.input( + 'firstname', + __('settings.profile.firstname'), + { + 'autocomplete': 'given-name', + 'max_length': 64, + 'value': f.formData('firstname', ''), + } + ) }} +
+
+ {{ f.input( + 'lastname', + __('settings.profile.lastname'), + { + 'autocomplete': 'family-name', + 'max_length': 64, + 'value': f.formData('lastname', ''), + } + ) }} +
+
+
+ {% endif %} + +
+

{{ __('page.sign-up.event-data') }}

+
+ {% if isGoodieEnabled %} +
+ {% set privacy_email = config('privacy_email') %} + {% set email_goody_label = __('settings.profile.email_goody') ~ + (privacy_email ? ' ' ~ __('settings.profile.privacy', [privacy_email]) : '') + %} + {{ f.checkbox( + 'email_goody', + email_goody_label, + { + 'raw_label': true, + 'checked': f.formData('email_goody', false), + }, + ) }} +
+
+ {% endif %} + + {% if isPlannedArrivalDateEnabled %} +
+ {{ f.input( + 'planned_arrival_date', + __('settings.profile.planned_arrival_date'), + { + 'type': 'date', + 'min': buildUpStartDate, + 'max': tearDownEndDate, + 'required': true, + 'required_icon': true, + 'value': f.formData('planned_arrival_date', buildUpStartDate), + } + ) }} +
+ {% endif %} + + {% if isGoodieTShirt %} +
+ {{ f.select( + 'tshirt_size', + __('settings.profile.shirt_size'), + tShirtSizes, + { + 'default_option': __('form.select_placeholder'), + 'required': true, + 'required_icon': true, + 'selected': f.formData('tshirt_size', ''), + } + ) }} +
+ {% endif %} + +
+ {{ f.input( + 'mobile', + __('settings.profile.mobile'), + { + 'type': 'tel-national', + 'max_length': 40, + 'value': f.formData('mobile', ''), + } + ) }} + {% if isShowMobileEnabled %} + {{ f.checkbox( + 'mobile_show', + __('settings.profile.mobile_show'), + { + 'raw_label': true, + 'checked': f.formData('mobile_show', false), + }, + ) }} + {% endif %} +
+ + {% if isDECTEnabled %} +
+ {{ f.input( + "dect", + __("settings.profile.dect"), + { + 'type': 'tel-local', + 'max_length': 40, + 'value': f.formData('dect', ''), + } + ) }} +
+ {% endif %} +
+
+ +
+

{{ __('page.sign-up.what-do-you-want-to-do') }}

+
+ {% for angelType in angelTypes %} +
+ {{ f.checkbox( + 'angel_types_' ~ angelType.id, + angelType.name ~ (angelType.restricted ? ' ' ~ m.icon('mortarboard-fill', 'text-body') : ''), + { + 'value': angelType.id, + 'raw_label': true, + 'checked': preselectedAngelTypes['angel_types_' ~ angelType.id] ?? false, + } + ) }} +
+ {% endfor %} +
+
+
+
+ {{ m.icon('mortarboard-fill', 'text-body') }} + {{ __('angeltypes.restricted.hint') }} +
+
+ {{ m.icon('info-circle', 'text-body') }} + {{ __('angeltypes.can-change-later') }} +
+
+
+
+ + {# + By assigning a name here, some magic™ will create a session var + "form-data-register-submit" with the value 1 on submit. + #} + {{ f.submit(__('page.sign-up.sign-up'), { + 'name': 'register-submit', + }) }} +
+
+{% endblock %} diff --git a/src/Controllers/OAuthController.php b/src/Controllers/OAuthController.php index ff1da1cf..87694146 100644 --- a/src/Controllers/OAuthController.php +++ b/src/Controllers/OAuthController.php @@ -328,6 +328,6 @@ class OAuthController extends BaseController $this->session->set('oauth2_enable_password', $config['enable_password']); $this->session->set('oauth2_allow_registration', $config['allow_registration']); - return $this->redirect->to('/register'); + return $this->redirect->to('/sign-up'); } } diff --git a/src/Controllers/SignUpController.php b/src/Controllers/SignUpController.php new file mode 100644 index 00000000..b79011fe --- /dev/null +++ b/src/Controllers/SignUpController.php @@ -0,0 +1,185 @@ +determineRegistrationDisabled()) { + return $this->notifySignUpDisabledAndRedirectToHome(); + } + + return $this->renderSignUpPage(); + } + + public function save(Request $request): Response + { + if ($this->determineRegistrationDisabled()) { + return $this->notifySignUpDisabledAndRedirectToHome(); + } + + $rawData = $request->getParsedBody(); + $user = $this->userFactory->createFromData($rawData); + + $this->addNotification('pages.sign-up.successful'); + + if ($this->config->get('welcome_msg')) { + // Set a session marker to display the welcome message on the next page + $this->session->set('show_welcome', true); + } + + if ($user->oauth?->count() > 0) { + // User has OAuth configured. Log in directly. + $provider = $user->oauth->first(); + return $this->redirect->to('/oauth/' . $provider->provider); + } + + return $this->redirect->to('/'); + } + + private function notifySignUpDisabledAndRedirectToHome(): Response + { + $this->addNotification('pages.sign-up.disabled', NotificationType::INFORMATION); + return $this->redirect->to('/'); + } + + private function renderSignUpPage(): Response + { + $goodieType = GoodieType::from($this->config->get('goodie_type')); + $preselectedAngelTypes = $this->determinePreselectedAngelTypes(); + + // form-data-register-submit is a marker, that the form was submitted. + // It will be used for instance to use the default angel types or the user selected ones. + // Clear it before render to reset the marker state. + $this->session->remove('form-data-register-submit'); + + return $this->response->withView( + 'pages/sign-up', + [ + 'tShirtSizes' => $this->config->get('tshirt_sizes'), + 'angelTypes' => AngelType::whereHideRegister(false)->get(), + 'preselectedAngelTypes' => $preselectedAngelTypes, + 'buildUpStartDate' => $this->userFactory->determineBuildUpStartDate()->format('Y-m-d'), + 'tearDownEndDate' => $this->config->get('teardown_end')?->format('Y-m-d'), + 'isPasswordEnabled' => $this->userFactory->determineIsPasswordEnabled(), + 'isDECTEnabled' => $this->config->get('enable_dect'), + 'isShowMobileEnabled' => $this->config->get('enable_mobile_show'), + 'isGoodieEnabled' => $goodieType !== GoodieType::None, + 'isGoodieTShirt' => $goodieType === GoodieType::Tshirt, + 'isPronounEnabled' => $this->config->get('enable_pronoun'), + 'isFullNameEnabled' => $this->config->get('enable_user_name'), + 'isPlannedArrivalDateEnabled' => $this->config->get('enable_planned_arrival'), + ], + ); + } + + /** + * @return Array Checkbox field name/Id → 1 + */ + private function determinePreselectedAngelTypes(): array + { + if ($this->session->has('form-data-register-submit')) { + // form-data-register-submit means a user just submitted the page. + // Preselect the angel types from the persisted session form data. + return $this->loadAngelTypesFromSessionFormData(); + } + + $preselectedAngelTypes = []; + + if ($this->session->has('oauth2_connect_provider')) { + $preselectedAngelTypes = $this->loadAngelTypesFromSessionOAuthGroups(); + } + + foreach (AngelType::whereRestricted(false)->whereHideRegister(false)->get() as $angelType) { + // preselect every angel type without restriction + $preselectedAngelTypes['angel_types_' . $angelType->id] = 1; + } + + return $preselectedAngelTypes; + } + + /** + * @return Array + */ + private function loadAngelTypesFromSessionOAuthGroups(): array + { + $oAuthAngelTypes = []; + $ssoTeams = $this->oAuth->getSsoTeams($this->session->get('oauth2_connect_provider')); + $oAuth2Groups = $this->session->get('oauth2_groups'); + + foreach ($ssoTeams as $name => $team) { + if (in_array($name, $oAuth2Groups)) { + // preselect angel type from oauth + $oAuthAngelTypes['angel_types_' . $team['id']] = 1; + } + } + + return $oAuthAngelTypes; + } + + /** + * @return Array + */ + private function loadAngelTypesFromSessionFormData(): array + { + $angelTypes = AngelType::whereHideRegister(false)->get(); + $selectedAngelTypes = []; + + foreach ($angelTypes as $angelType) { + $sessionKey = 'form-data-angel_types_' . $angelType->id; + + if ($this->session->has($sessionKey)) { + $selectedAngelTypes['angel_types_' . $angelType->id] = 1; + // remove from session so that it doesn't stay there forever + $this->session->remove($sessionKey); + } + } + + return $selectedAngelTypes; + } + + private function determineRegistrationDisabled(): bool + { + $authUser = $this->auth->user(); + $isOAuth = $this->session->get('oauth2_connect_provider'); + $isPasswordEnabled = $this->userFactory->determineIsPasswordEnabled(); + + return !auth()->can('register') // No registration permission + // Not authenticated and + // Registration disabled + || ( + !$authUser + && !$this->config->get('registration_enabled') + && !$this->session->get('oauth2_allow_registration') + ) + // Password disabled and not oauth + || (!$authUser && !$isPasswordEnabled && !$isOAuth); + } +} diff --git a/src/Factories/User.php b/src/Factories/User.php new file mode 100644 index 00000000..44f971a5 --- /dev/null +++ b/src/Factories/User.php @@ -0,0 +1,337 @@ + $rawData Raw data from which a user should be created + * @return The user if successful + * @throws + */ + public function createFromData(array $rawData): EngelsystemUser + { + $data = $this->validateUser($rawData); + return $this->createUser($data, $rawData); + } + + public function determineIsPasswordEnabled(): bool + { + $isPasswordEnabled = $this->config->get('enable_password'); + $oAuthEnablePassword = $this->session->get('oauth2_enable_password'); + + if (!is_null($oAuthEnablePassword)) { + // o-auth overwrites config + $isPasswordEnabled = $oAuthEnablePassword; + } + + return $isPasswordEnabled; + } + + public function determineBuildUpStartDate(): DateTimeInterface + { + return $this->config->get('buildup_start') ?? CarbonImmutable::now(); + } + + /** + * @param Array $rawData + * @throws ValidationException + */ + private function validateUser(array $rawData): array + { + $validationRules = [ + 'username' => 'required|username', + 'email' => 'required|email', + 'email_shiftinfo' => 'optional|checked', + 'email_by_human_allowed' => 'optional|checked', + 'email_messages' => 'optional|checked', + 'email_news' => 'optional|checked', + 'email_goody' => 'optional|checked', + // Using length here, because min/max would validate dect/mobile as numbers. + 'mobile' => 'optional|length:0:40', + ]; + + $isPasswordEnabled = $this->determineIsPasswordEnabled(); + + if ($isPasswordEnabled) { + $minPasswordLength = $this->config->get('min_password_length'); + $validationRules['password'] = 'required|length:' . $minPasswordLength; + $validationRules['password_confirmation'] = 'required'; + } + + $isFullNameEnabled = $this->config->get('enable_user_name'); + + if ($isFullNameEnabled) { + $validationRules['firstname'] = 'optional|length:0:64'; + $validationRules['lastname'] = 'optional|length:0:64'; + } + + $isPronounEnabled = $this->config->get('enable_pronoun'); + + if ($isPronounEnabled) { + $validationRules['pronoun'] = 'optional|max:15'; + } + + $isShowMobileEnabled = $this->config->get('enable_mobile_show'); + + if ($isShowMobileEnabled) { + $validationRules['mobile_show'] = 'optional|checked'; + } + + $isPlannedArrivalDateEnabled = $this->config->get('enable_planned_arrival'); + + if ($isPlannedArrivalDateEnabled) { + $isoBuildUpStartDate = $this->determineBuildUpStartDate(); + /** @var DateTimeInterface|null $tearDownEndDate */ + $tearDownEndDate = $this->config->get('teardown_end'); + + if ($tearDownEndDate) { + $validationRules['planned_arrival_date'] = sprintf( + 'required|date|between:%s:%s', + $isoBuildUpStartDate->format('Y-m-d'), + $tearDownEndDate->format('Y-m-d') + ); + } else { + $validationRules['planned_arrival_date'] = sprintf( + 'required|date|min:%s', + $isoBuildUpStartDate->format('Y-m-d'), + ); + } + } + + $isDECTEnabled = $this->config->get('enable_dect'); + + if ($isDECTEnabled) { + // Using length here, because min/max would validate dect/mobile as numbers. + $validationRules['dect'] = 'optional|length:0:40'; + } + + $goodieType = GoodieType::from($this->config->get('goodie_type')); + $isGoodieTShirt = $goodieType === GoodieType::Tshirt; + + if ($isGoodieTShirt) { + $validationRules['tshirt_size'] = 'required|shirt-size'; + } + + $data = $this->validate($rawData, $validationRules); + + // additional validations + $this->validateUniqueUsername($data['username']); + $this->validateUniqueEmail($data['email']); + + if ($isPasswordEnabled) { + // Finally, validate that password matches password_confirmation. + // The respect keyValue validation does not seem to work. + $this->validatePasswordMatchesConfirmation($rawData); + } + + return $data; + } + + /** + * @param Array $rawData + */ + private function validatePasswordMatchesConfirmation(array $rawData): void + { + if ($rawData['password'] !== $rawData['password_confirmation']) { + throw new ValidationException( + (new Validator())->addErrors(['password' => [ + 'settings.password.confirmation-does-not-match', + ]]) + ); + } + } + + private function validateUniqueUsername(string $username): void + { + if (EngelsystemUser::whereName($username)->exists()) { + throw new ValidationException( + (new Validator())->addErrors(['username' => [ + 'settings.profile.nick.already-taken', + ]]) + ); + } + } + + private function validateUniqueEmail(string $email): void + { + if (EngelsystemUser::whereEmail($email)->exists()) { + throw new ValidationException( + (new Validator())->addErrors(['email' => [ + 'settings.profile.email.already-taken', + ]]) + ); + } + } + + /** + * @param Array $data + * @param Array $rawData + */ + private function createUser(array $data, array $rawData): EngelsystemUser + { + $this->dbConnection->beginTransaction(); + + $user = new EngelsystemUser([ + 'name' => $data['username'], + 'password' => '', + 'email' => $data['email'], + 'api_key' => '', + 'last_login_at' => null, + ]); + $user->save(); + + $contact = new Contact([ + 'dect' => $data['dect'] ?? null, + 'mobile' => $data['mobile'], + ]); + $contact->user() + ->associate($user) + ->save(); + + $isPlannedArrivalDateEnabled = $this->config->get('enable_planned_arrival'); + $plannedArrivalDate = null; + + if ($isPlannedArrivalDateEnabled) { + $plannedArrivalDate = Carbon::createFromFormat('Y-m-d', $data['planned_arrival_date']); + } + + $personalData = new PersonalData([ + 'first_name' => $data['firstname'] ?? null, + 'last_name' => $data['lastname'] ?? null, + 'pronoun' => $data['pronoun'] ?? null, + 'shirt_size' => $data['tshirt_size'] ?? null, + 'planned_arrival_date' => $plannedArrivalDate, + ]); + $personalData->user() + ->associate($user) + ->save(); + + $isShowMobileEnabled = $this->config->get('enable_mobile_show'); + + $settings = new Settings([ + 'language' => $this->session->get('locale') ?? 'en_US', + 'theme' => $this->config->get('theme'), + 'email_human' => $data['email_by_human_allowed'] ?? false, + 'email_messages' => $data['email_messages'] ?? false, + 'email_goody' => $data['email_goody'] ?? false, + 'email_shiftinfo' => $data['email_shiftinfo'] ?? false, + 'email_news' => $data['email_news'] ?? false, + 'mobile_show' => $isShowMobileEnabled && $data['mobile_show'], + ]); + $settings->user() + ->associate($user) + ->save(); + + $state = new State([]); + + if ($this->config->get('autoarrive')) { + $state->arrived = true; + $state->arrival_date = CarbonImmutable::now(); + } + + $state->user() + ->associate($user) + ->save(); + + if ($this->session->has('oauth2_connect_provider') && $this->session->has('oauth2_user_id')) { + $oauth = new OAuth([ + 'provider' => $this->session->get('oauth2_connect_provider'), + 'identifier' => $this->session->get('oauth2_user_id'), + 'access_token' => $this->session->get('oauth2_access_token'), + 'refresh_token' => $this->session->get('oauth2_refresh_token'), + 'expires_at' => $this->session->get('oauth2_expires_at'), + ]); + $oauth->user() + ->associate($user) + ->save(); + + $this->session->remove('oauth2_connect_provider'); + $this->session->remove('oauth2_user_id'); + $this->session->remove('oauth2_access_token'); + $this->session->remove('oauth2_refresh_token'); + $this->session->remove('oauth2_expires_at'); + } + + $defaultGroup = Group::find($this->authenticator->getDefaultRole()); + $user->groups()->attach($defaultGroup); + + if ($this->determineIsPasswordEnabled() && array_key_exists('password', $data)) { + auth()->setPassword($user, $data['password']); + } + + $assignedAngelTypeNames = $this->assignAngelTypes($user, $rawData); + + $this->logger->info(sprintf( + 'User %s signed up as: %s', + sprintf('%s (%u)', $user->displayName, $user->id), + join(', ', $assignedAngelTypeNames), + )); + + $this->dbConnection->commit(); + + return $user; + } + + private function assignAngelTypes(EngelsystemUser $user, array $rawData): array + { + $possibleAngelTypes = AngelType::whereHideRegister(false)->get(); + $assignedAngelTypeNames = []; + + foreach ($possibleAngelTypes as $possibleAngelType) { + $angelTypeCheckboxId = 'angel_types_' . $possibleAngelType->id; + + if (array_key_exists($angelTypeCheckboxId, $rawData)) { + $user->userAngelTypes()->attach($possibleAngelType); + $assignedAngelTypeNames[] = $possibleAngelType->name; + } + } + + return $assignedAngelTypeNames; + } + + private function validate(array $rawData, array $rules): array + { + $isValid = $this->validator->validate($rawData, $rules); + + if (!$isValid) { + throw new ValidationException($this->validator); + } + + return $this->validator->getData(); + } +} diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php index 34b861fe..0a7dded5 100644 --- a/src/Middleware/LegacyMiddleware.php +++ b/src/Middleware/LegacyMiddleware.php @@ -108,10 +108,6 @@ class LegacyMiddleware implements MiddlewareInterface $title = shifts_title(); $content = user_shifts(); return [$title, $content]; - case 'register': - $title = register_title(); - $content = guest_register(); - return [$title, $content]; case 'admin_user': $title = admin_user_title(); $content = admin_user(); diff --git a/src/Models/OAuth.php b/src/Models/OAuth.php index 4c3a329a..1f2c7f16 100644 --- a/src/Models/OAuth.php +++ b/src/Models/OAuth.php @@ -6,6 +6,7 @@ namespace Engelsystem\Models; use Carbon\Carbon; use Engelsystem\Models\User\UsesUserModel; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Query\Builder as QueryBuilder; /** @@ -26,6 +27,7 @@ use Illuminate\Database\Query\Builder as QueryBuilder; */ class OAuth extends BaseModel { + use HasFactory; use UsesUserModel; public $table = 'oauth'; // phpcs:ignore diff --git a/tests/Feature/Controllers/SignUpControllerTest.php b/tests/Feature/Controllers/SignUpControllerTest.php new file mode 100644 index 00000000..d30754af --- /dev/null +++ b/tests/Feature/Controllers/SignUpControllerTest.php @@ -0,0 +1,248 @@ + + */ + private array $modelsToBeDeleted; + private SignUpController $subject; + + public function setUp(): void + { + parent::setUp(); + $this->modelsToBeDeleted = []; + $this->application = app(); + $this->oauth = $this->getMockBuilder(OAuth2::class) + ->disableOriginalConstructor() + ->getMock(); + $this->application->instance(OAuth2::class, $this->oauth); + $this->config = $this->application->get(Config::class); + $this->session = $this->application->get(SessionInterface::class); + $this->subject = $this->application->make(SignUpController::class); + } + + public function tearDown(): void + { + parent::tearDown(); + $this->deleteModels(); + } + + /** + * Renders the sign-up page with a minimum fields config. + * Asserts that the basic fields are there while the other fields are not there. + * + * @covers \Engelsystem\Controllers\SignUpController + */ + public function testViewMinimumConfig(): void + { + SignUpConfig::setMinimumConfig($this->config); + $response = $this->subject->view(); + + $this->assertSame(200, $response->getStatusCode()); + $responseHTML = $response->getBody()->__toString(); + + // assert the expected fields are there + FormFieldAssert::assertContainsInputField('username', $responseHTML); + FormFieldAssert::assertContainsInputField('password', $responseHTML); + FormFieldAssert::assertContainsInputField('password_confirmation', $responseHTML); + FormFieldAssert::assertContainsInputField('email', $responseHTML); + FormFieldAssert::assertContainsInputField('mobile', $responseHTML); + + // assert the disabled fields are not there + FormFieldAssert::assertNotContainsInputField('pronoun', $responseHTML); + FormFieldAssert::assertNotContainsInputField('firstname', $responseHTML); + FormFieldAssert::assertNotContainsInputField('lastname', $responseHTML); + FormFieldAssert::assertNotContainsInputField('email_goody', $responseHTML); + FormFieldAssert::assertNotContainsSelectField('tshirt_size', $responseHTML); + FormFieldAssert::assertNotContainsInputField('planned_arrival_date', $responseHTML); + FormFieldAssert::assertNotContainsInputField('mobile_show', $responseHTML); + FormFieldAssert::assertNotContainsInputField('dect', $responseHTML); + } + + /** + * Renders the sign-up page with a maximum fields config. + * Asserts that all fields are there. + * + * @covers \Engelsystem\Controllers\SignUpController + */ + public function testViewMaximumConfig(): void + { + SignUpConfig::setMaximumConfig($this->config); + $response = $this->subject->view(); + + $this->assertSame(200, $response->getStatusCode()); + $responseHTML = $response->getBody()->__toString(); + + // assert the expected fields are there + FormFieldAssert::assertContainsInputField('pronoun', $responseHTML); + FormFieldAssert::assertContainsInputField('username', $responseHTML); + FormFieldAssert::assertContainsInputField('email', $responseHTML); + FormFieldAssert::assertContainsInputField('mobile', $responseHTML); + FormFieldAssert::assertContainsInputField('password', $responseHTML); + FormFieldAssert::assertContainsInputField('password_confirmation', $responseHTML); + FormFieldAssert::assertContainsInputField('firstname', $responseHTML); + FormFieldAssert::assertContainsInputField('lastname', $responseHTML); + FormFieldAssert::assertContainsInputField('email_goody', $responseHTML); + FormFieldAssert::assertContainsSelectField('tshirt_size', $responseHTML); + FormFieldAssert::assertContainsInputField('planned_arrival_date', $responseHTML); + FormFieldAssert::assertContainsInputField('mobile_show', $responseHTML); + FormFieldAssert::assertContainsInputField('dect', $responseHTML); + } + + /** + * @covers \Engelsystem\Controllers\SignUpController + */ + public function testViewAngelTypesOAuthPreselection(): void + { + SignUpConfig::setMinimumConfig($this->config); + $angelTypes = $this->createAngelTypes(); + $this->session->set('oauth2_connect_provider', 'test_oauth_provider'); + $this->session->set('oauth2_groups', [$angelTypes[1]->name]); + $this->oauth + ->method('getSsoTeams') + ->with('test_oauth_provider') + ->willReturn( + [ + $angelTypes[1]->name => ['id' => $angelTypes[1]->id], + $angelTypes[2]->name => ['id' => $angelTypes[2]->id], + ], + ); + + $response = $this->subject->view(); + + $this->assertSame(200, $response->getStatusCode()); + $responseHTML = $response->getBody()->__toString(); + + // assert that the unrestricted angel type is there and checked + FormFieldAssert::assertContainsCheckedCheckbox('angel_types_' . $angelTypes[0]->id, $responseHTML); + + // assert that the first restricted angel type from oauth is there and checked + FormFieldAssert::assertContainsCheckedCheckbox('angel_types_' . $angelTypes[1]->id, $responseHTML); + + // assert that the second restricted angel type not in oauth is there and not checked + FormFieldAssert::assertContainsUncheckedCheckbox('angel_types_' . $angelTypes[2]->id, $responseHTML); + + // assert that the angel type with "hide_register" = true is not there + FormFieldAssert::assertNotContainsInputField('angel_types_' . $angelTypes[3]->id, $responseHTML); + } + + /** + * @covers \Engelsystem\Controllers\SignUpController + */ + public function testViewAngelTypesPreselection(): void + { + $angelTypes = $this->createAngelTypes(); + + SignUpConfig::setMinimumConfig($this->config); + $response = $this->subject->view(); + + $this->assertSame(200, $response->getStatusCode()); + $responseHTML = $response->getBody()->__toString(); + + // assert that the unrestricted angel type is there and checked + FormFieldAssert::assertContainsCheckedCheckbox('angel_types_' . $angelTypes[0]->id, $responseHTML); + + // assert that restricted angel type are there and not checked + FormFieldAssert::assertContainsUncheckedCheckbox('angel_types_' . $angelTypes[1]->id, $responseHTML); + FormFieldAssert::assertContainsUncheckedCheckbox('angel_types_' . $angelTypes[2]->id, $responseHTML); + + // assert that the angel type with "hide_register" = true is not there + FormFieldAssert::assertNotContainsInputField('angel_types_' . $angelTypes[3]->id, $responseHTML); + } + + /** + * Asserts that values are prefilled after submit + * + * @covers \Engelsystem\Controllers\SignUpController + */ + public function testViewValuesAfterSubmit(): void + { + $angelTypes = $this->createAngelTypes(); + + // fake submit and set form-data in session + $this->session->set('form-data-register-submit', '1'); + $this->session->set('form-data-angel_types_' . $angelTypes[1]->id, '1'); + + SignUpConfig::setMinimumConfig($this->config); + $response = $this->subject->view(); + + $this->assertSame(200, $response->getStatusCode()); + $responseHTML = $response->getBody()->__toString(); + + // assert that the unrestricted angel type is not checked + FormFieldAssert::assertContainsUncheckedCheckbox('angel_types_' . $angelTypes[0]->id, $responseHTML); + + // assert that the restricted angel type is checked + FormFieldAssert::assertContainsCheckedCheckbox('angel_types_' . $angelTypes[1]->id, $responseHTML); + } + + /** + * Creates three angel types: + * - unrestricted + * - restricted + * - unrestricted, hidden on sign-up + * + * @return Array + */ + private function createAngelTypes(): array + { + $angelType1 = AngelType::create([ + 'name' => 'Test angel type 1', + ]); + + $angelType2 = AngelType::create([ + 'name' => 'Test angel type 2', + 'restricted' => true, + ]); + + $angelType3 = AngelType::create([ + 'name' => 'Test angel type 3', + 'restricted' => true, + ]); + + $angelType4 = AngelType::create([ + 'name' => 'Test angel type 4', + 'hide_register' => true, + ]); + + $this->modelsToBeDeleted[] = $angelType1; + $this->modelsToBeDeleted[] = $angelType2; + $this->modelsToBeDeleted[] = $angelType3; + $this->modelsToBeDeleted[] = $angelType4; + return [$angelType1, $angelType2, $angelType3, $angelType4]; + } + + private function deleteModels(): void + { + foreach ($this->modelsToBeDeleted as $modelToBeDeleted) { + $modelToBeDeleted->delete(); + } + } +} diff --git a/tests/Unit/Controllers/ControllerTest.php b/tests/Unit/Controllers/ControllerTest.php index fa428f86..f6bf8447 100644 --- a/tests/Unit/Controllers/ControllerTest.php +++ b/tests/Unit/Controllers/ControllerTest.php @@ -17,6 +17,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; use Psr\Log\Test\TestLogger; use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; abstract class ControllerTest extends TestCase @@ -78,6 +79,7 @@ abstract class ControllerTest extends TestCase $this->session = new Session(new MockArraySessionStorage()); $this->app->instance('session', $this->session); $this->app->instance(Session::class, $this->session); + $this->app->instance(SessionInterface::class, $this->session); $this->app->bind(UrlGeneratorInterface::class, UrlGenerator::class); diff --git a/tests/Unit/Controllers/OAuthControllerTest.php b/tests/Unit/Controllers/OAuthControllerTest.php index 99f2a1dd..75b4c4e1 100644 --- a/tests/Unit/Controllers/OAuthControllerTest.php +++ b/tests/Unit/Controllers/OAuthControllerTest.php @@ -440,7 +440,7 @@ class OAuthControllerTest extends TestCase $this->setExpects($this->auth, 'user', null, null, $this->atLeastOnce()); - $this->setExpects($this->redirect, 'to', ['/register']); + $this->setExpects($this->redirect, 'to', ['/sign-up']); $request = new Request(); $request = $request @@ -533,7 +533,7 @@ class OAuthControllerTest extends TestCase $this->setExpects($this->auth, 'user', null, null, $this->atLeastOnce()); - $this->setExpects($this->redirect, 'to', ['/register']); + $this->setExpects($this->redirect, 'to', ['/sign-up']); $request = new Request(); $request = $request diff --git a/tests/Unit/Controllers/SignUpControllerTest.php b/tests/Unit/Controllers/SignUpControllerTest.php new file mode 100644 index 00000000..23b34726 --- /dev/null +++ b/tests/Unit/Controllers/SignUpControllerTest.php @@ -0,0 +1,205 @@ +mockTranslator(); + + $this->authenticator = $this->getMockBuilder(Authenticator::class) + ->disableOriginalConstructor() + ->getMock(); + $this->app->instance(Authenticator::class, $this->authenticator); + $this->app->alias(Authenticator::class, 'authenticator'); + + $this->userFactory = $this->getMockBuilder(User::class) + ->disableOriginalConstructor() + ->getMock(); + $this->config->set('oauth', []); + $this->app->instance(User::class, $this->userFactory); + $this->subject = $this->app->make(SignUpController::class); + } + + /** + * @covers \Engelsystem\Controllers\SignUpController + */ + public function testSave(): void + { + $this->setPasswordRegistrationEnabledConfig(); + + $userData = ['user' => 'data']; + $request = $this->request->withParsedBody($userData); + + // Assert the controller passes the submitted data to the user factory + $this->userFactory + ->expects(self::once()) + ->method('createFromData') + ->with($userData) + ->willReturn(new EngelsystemUser()); + + // Assert that the user is redirected to home + $this->response + ->expects(self::once()) + ->method('redirectTo') + ->with('http://localhost/', 302); + + $this->subject->save($request); + + // Assert that the success notification is there + self::assertEquals( + [ + 'messages.message' => ['pages.sign-up.successful'], + ], + $this->session->all() + ); + + // Assert that "show_welcome" is not set in session, + // because "welcome_msg" is not configured. + $this->assertFalse($this->session->has('show_welcome')); + } + + /** + * @covers \Engelsystem\Controllers\SignUpController + */ + public function testSaveOAuth(): void + { + $this->setPasswordRegistrationEnabledConfig(); + + $userData = ['user' => 'data']; + $request = $this->request->withParsedBody($userData); + + $user = (new EngelsystemUser())->factory()->create(); + $oauth = (new OAuth())->factory()->create([ + 'user_id' => $user->id, + ]); + + $this->userFactory + ->expects(self::once()) + ->method('createFromData') + ->with($userData) + ->willReturn($user); + + // Assert that the user is redirected to the OAuth login + $this->response + ->expects(self::once()) + ->method('redirectTo') + ->with('http://localhost/oauth/' . $oauth->provider, 302); + + $this->subject->save($request); + } + + /** + * @covers \Engelsystem\Controllers\SignUpController + */ + public function testSaveWithWelcomeMesssage(): void + { + $this->setPasswordRegistrationEnabledConfig(); + $this->config->set('welcome_msg', true); + + $userData = ['user' => 'data']; + $request = $this->request->withParsedBody($userData); + $this->subject->save($request); + + // Assert that "show_welcome" is set in session, + // because "welcome_msg" is enabled in the config. + $this->assertTrue($this->session->get('show_welcome')); + } + + /** + * @covers \Engelsystem\Controllers\SignUpController + */ + public function testSaveRegistrationDisabled(): void + { + $this->config->set('registration_enabled', false); + $request = $this->request->withParsedBody([]); + + // Assert the controller does not call createFromData + $this->userFactory + ->expects(self::never()) + ->method('createFromData'); + + // Assert that the user is redirected to home + $this->response + ->expects(self::once()) + ->method('redirectTo') + ->with('http://localhost/', 302); + + $this->subject->save($request); + + // Assert that the error notification is there + self::assertEquals( + [ + 'messages.information' => ['pages.sign-up.disabled'], + ], + $this->session->all() + ); + } + + /** + * @covers \Engelsystem\Controllers\SignUpController + */ + public function testViewRegistrationDisabled(): void + { + $this->config->set('registration_enabled', false); + $request = $this->request->withParsedBody([]); + + // Assert the controller does not call createFromData + $this->userFactory + ->expects(self::never()) + ->method('createFromData'); + + // Assert that the user is redirected to home + $this->response + ->expects(self::once()) + ->method('redirectTo') + ->with('http://localhost/', 302); + + $this->subject->view($request); + + // Assert that the error notification is there + self::assertEquals( + [ + 'messages.information' => ['pages.sign-up.disabled'], + ], + $this->session->all() + ); + } + + private function setPasswordRegistrationEnabledConfig(): void + { + $this->config->set('registration_enabled', true); + $this->authenticator + ->method('can') + ->with('register') + ->willReturn(true); + $this->userFactory->method('determineIsPasswordEnabled') + ->willReturn(true); + } +} diff --git a/tests/Unit/Factories/UserTest.php b/tests/Unit/Factories/UserTest.php new file mode 100644 index 00000000..38599aef --- /dev/null +++ b/tests/Unit/Factories/UserTest.php @@ -0,0 +1,496 @@ +now = CarbonImmutable::now(); + Carbon::setTestNow($this->now); + + $this->initDatabase(); + $this->config = new Config([]); + $this->app->instance(Config::class, $this->config); + $this->app->alias(Config::class, 'config'); + $this->config->set('oauth', []); + $this->session = new Session(new MockArraySessionStorage()); + $this->app->instance(SessionInterface::class, $this->session); + $this->app->instance(LoggerInterface::class, $this->getMockForAbstractClass(LoggerInterface::class)); + + $this->app->instance(ServerRequestInterface::class, new Request()); + $this->app->instance(Authenticator::class, $this->app->make(Authenticator::class)); + $this->app->alias(Authenticator::class, 'authenticator'); + + $this->subject = $this->app->make(User::class); + } + + public function tearDown(): void + { + Carbon::setTestNow(); + } + + /** + * Minimal config with empty data. + * + * @covers \Engelsystem\Factories\User + */ + public function testMinimumConfigEmpty(): void + { + SignUpConfig::setMinimumConfig($this->config); + + $this->assertDataRaisesValidationException( + [], + [ + 'username' => [ + 'validation.username.required', + 'validation.username.username', + ], + 'email' => [ + 'validation.email.required', + 'validation.email.email', + ], + 'password' => [ + 'validation.password.required', + 'validation.password.length', + ], + 'password_confirmation' => [ + 'validation.password_confirmation.required', + ], + ] + ); + } + + /** + * Minimal config with valid data. + * + * @covers \Engelsystem\Factories\User + */ + public function testMinimumConfigCreate(): void + { + SignUpConfig::setMinimumConfig($this->config); + + $user = $this->subject->createFromData([ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + ]); + + $this->assertSame('fritz', $user->name); + $this->assertSame('fritz@example.com', $user->email); + $this->assertSame(false, $user->state->arrived); + } + + /** + * Maximum config with empty data. + * + * @covers \Engelsystem\Factories\User + */ + public function testMaximumConfigEmpty(): void + { + SignUpConfig::setMaximumConfig($this->config); + + $this->assertDataRaisesValidationException( + [], + [ + 'username' => [ + 'validation.username.required', + 'validation.username.username', + ], + 'email' => [ + 'validation.email.required', + 'validation.email.email', + ], + 'password' => [ + 'validation.password.required', + 'validation.password.length', + ], + 'password_confirmation' => [ + 'validation.password_confirmation.required', + ], + 'planned_arrival_date' => [ + 'validation.planned_arrival_date.required', + 'validation.planned_arrival_date.date', + 'validation.planned_arrival_date.min', + ], + 'tshirt_size' => [ + 'validation.tshirt_size.required', + 'validation.tshirt_size.shirtSize', + ], + ] + ); + } + + /** + * Maximum config with invalid data. + * + * @covers \Engelsystem\Factories\User + */ + public function testMaximumConfigInvalid(): void + { + SignUpConfig::setMaximumConfig($this->config); + + $this->assertDataRaisesValidationException( + [ + 'username' => 'fritz23', + 'pronoun' => str_repeat('a', 20), + 'firstname' => str_repeat('a', 70), + 'lastname' => str_repeat('a', 70), + 'email' => 'notanemail', + 'password' => 'a', + 'tshirt_size' => 'A', + 'planned_arrival_date' => $this->now->subDays(7), + 'dect' => str_repeat('a', 50), + 'mobile' => str_repeat('a', 50), + ], + [ + 'username' => [ + 'validation.username.username', + ], + 'email' => [ + 'validation.email.email', + ], + 'mobile' => [ + 'validation.mobile.optional', + ], + 'password' => [ + 'validation.password.length', + ], + 'password_confirmation' => [ + 'validation.password_confirmation.required', + ], + 'firstname' => [ + 'validation.firstname.optional', + ], + 'lastname' => [ + 'validation.lastname.optional', + ], + 'pronoun' => [ + 'validation.pronoun.optional', + ], + 'planned_arrival_date' => [ + 'validation.planned_arrival_date.min', + ], + 'dect' => [ + 'validation.dect.optional', + ], + 'tshirt_size' => [ + 'validation.tshirt_size.shirtSize', + ], + ] + ); + } + + /** + * Minimal config with valid data. + * + * @covers \Engelsystem\Factories\User + */ + public function testMaximumConfigCreate(): void + { + SignUpConfig::setMaximumConfig($this->config); + + $user = $this->subject->createFromData([ + 'pronoun' => 'they', + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + 'planned_arrival_date' => $this->now->format('Y-m-d'), + 'tshirt_size' => 'M', + 'mobile_show' => 1, + ]); + + $this->assertSame('they', $user->personalData->pronoun); + $this->assertSame('fritz', $user->name); + $this->assertSame('fritz@example.com', $user->email); + $this->assertTrue(password_verify('s3cret', $user->password)); + $this->assertSame( + $this->now->format('Y-m-d'), + $user->personalData->planned_arrival_date->format('Y-m-d') + ); + $this->assertTrue($user->settings->mobile_show); + } + + /** + * @covers \Engelsystem\Factories\User + */ + public function testPasswordDoesNotMatchConfirmation(): void + { + SignUpConfig::setMinimumConfig($this->config); + + $this->assertDataRaisesValidationException( + [ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 'huhuuu', + ], + [ + 'password' => [ + 'settings.password.confirmation-does-not-match', + ], + ] + ); + } + + /** + * @covers \Engelsystem\Factories\User + */ + public function testUsernameAlreadyTaken(): void + { + SignUpConfig::setMinimumConfig($this->config); + $this->createFritz(); + + $this->assertDataRaisesValidationException( + [ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + ], + [ + 'username' => [ + 'settings.profile.nick.already-taken', + ], + ] + ); + } + + /** + * @covers \Engelsystem\Factories\User + */ + public function testEmailAlreadyTaken(): void + { + SignUpConfig::setMinimumConfig($this->config); + $this->createFritz(); + + $this->assertDataRaisesValidationException( + [ + 'username' => 'peter', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + ], + [ + 'email' => [ + 'settings.profile.email.already-taken', + ], + ] + ); + } + + /** + * @covers \Engelsystem\Factories\User + */ + public function testAngelTypeAssignment(): void + { + $angelTypes = $this->createAngelTypes(); + SignUpConfig::setMinimumConfig($this->config); + + $user = $this->subject->createFromData([ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + 'angel_types_' . $angelTypes[0]->id => 1, + 'angel_types_' . $angelTypes[1]->id => 1, + // some angel type, that does not exist + 'angel_types_asd' => 1, + ]); + + // Expect an assignment of the normal angel type + $this->assertTrue( + $user->userAngelTypes->contains('name', $angelTypes[0]->name) + ); + + // Do not expect an assignment of the angel type hidden on sign-up + $this->assertFalse( + $user->userAngelTypes->contains('name', $angelTypes[1]->name) + ); + } + + /** + * @covers \Engelsystem\Factories\User + */ + public function testDisablePasswortViaOAuth(): void + { + SignUpConfig::setMinimumConfig($this->config); + $this->config->set('enable_password', false); + $this->session->set('oauth2_enable_password', true); + + $user = $this->subject->createFromData([ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + ]); + + $this->assertSame('fritz', $user->name); + $this->assertSame('fritz@example.com', $user->email); + $this->assertTrue(password_verify('s3cret', $user->password)); + } + + /** + * @covers \Engelsystem\Factories\User + */ + public function testAutoArrive(): void + { + SignUpConfig::setMinimumConfig($this->config); + $this->config->set('autoarrive', true); + + $user = $this->subject->createFromData([ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + ]); + + $this->assertSame('fritz', $user->name); + $this->assertSame('fritz@example.com', $user->email); + $this->assertSame(true, $user->state->arrived); + $this->assertEqualsWithDelta( + $this->now->timestamp, + $user->state->arrival_date->timestamp, + 1, + ); + } + + /** + * Covers the case where both, build-up and tear-down dates are configured. + * + * @covers \Engelsystem\Factories\User + */ + public function testBuildUpAndTearDownDates(): void + { + SignUpConfig::setMinimumConfig($this->config); + $this->config->set('enable_planned_arrival', true); + $this->config->set('buildup_start', $this->now); + $this->config->set('teardown_end', $this->now->addDays(7)); + + $this->assertDataRaisesValidationException( + [ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + 'planned_arrival_date' => $this->now->subDays(7), + ], + [ + 'planned_arrival_date' => [ + 'validation.planned_arrival_date.between', + ], + ] + ); + } + + /** + * @covers \Engelsystem\Factories\User + */ + public function testOAuth(): void + { + SignUpConfig::setMinimumConfig($this->config); + $this->session->set('oauth2_connect_provider', 'sso'); + $this->session->set('oauth2_user_id', 'fritz_sso'); + $this->session->set('oauth2_access_token', 'abc123'); + $this->session->set('oauth2_refresh_token', 'jkl456'); + $this->session->set('oauth2_expires_at', '2023-08-15 08:00:00'); + + $user = $this->subject->createFromData([ + 'username' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => 's3cret', + 'password_confirmation' => 's3cret', + ]); + + $oAuth = $user->oauth->first(); + $this->assertNotNull($oAuth); + $this->assertSame('sso', $oAuth->provider); + $this->assertSame('fritz_sso', $oAuth->identifier); + $this->assertSame('abc123', $oAuth->access_token); + $this->assertSame('jkl456', $oAuth->refresh_token); + $this->assertSame('2023-08-15 08:00:00', $oAuth->expires_at->format('Y-m-d H:i:s')); + } + + /** + * Create a user with nick "fritz" and email "fritz@example.com". + */ + private function createFritz(): void + { + UserModel::create([ + 'name' => 'fritz', + 'email' => 'fritz@example.com', + 'password' => '', + 'api_key' => '', + ]); + } + + /** + * Creates two AngelTypes: + * 1. Normal angel type + * 2. Angel type hidden on sign-up + * + * @return Array + */ + private function createAngelTypes(): array + { + return [ + AngelType::create([ + 'name' => 'Test angel type 1', + ]), + AngelType::create([ + 'name' => 'Test angel type 2', + 'hide_register' => true, + ]), + ]; + } + + /** + * @param Array $data Data passed to User::createFromData + * @param Array> $expectedValidationErrors Expected validation errors + */ + private function assertDataRaisesValidationException(array $data, array $expectedValidationErrors): void + { + try { + $this->subject->createFromData($data); + self::fail('Expected exception not raised'); + } catch (ValidationException $err) { + $validator = $err->getValidator(); + $validationErrors = $validator->getErrors(); + $this->assertSame($expectedValidationErrors, $validationErrors); + } + } +} diff --git a/tests/Unit/HasDatabase.php b/tests/Unit/HasDatabase.php index fc86c2bf..804d4509 100644 --- a/tests/Unit/HasDatabase.php +++ b/tests/Unit/HasDatabase.php @@ -9,6 +9,7 @@ use Engelsystem\Database\Migration\Migrate; use Engelsystem\Database\Migration\MigrationServiceProvider; use Engelsystem\Http\Request; use Illuminate\Database\Capsule\Manager as CapsuleManager; +use Illuminate\Database\Connection; use PDO; use Psr\Http\Message\ServerRequestInterface; @@ -30,6 +31,7 @@ trait HasDatabase $this->database = new Database($connection); $this->app->instance(Database::class, $this->database); + $this->app->instance(Connection::class, $connection); $this->app->register(MigrationServiceProvider::class); $this->app->instance(ServerRequestInterface::class, new Request()); diff --git a/tests/Utils/FormFieldAssert.php b/tests/Utils/FormFieldAssert.php new file mode 100644 index 00000000..39728273 --- /dev/null +++ b/tests/Utils/FormFieldAssert.php @@ -0,0 +1,79 @@ +]*name="$NAME"/s', [ + '$TAG' => $tag, + '$NAME' => $name, + ]); + } + + private static function makeCheckedCheckboxPattern(string $name): string + { + return strtr('/]*type="checkbox"[^>]*name="$NAME"[^>]*checked.*?/s', [ + '$NAME' => $name, + ]); + } +} diff --git a/tests/Utils/SignUpConfig.php b/tests/Utils/SignUpConfig.php new file mode 100644 index 00000000..5a9aeb6b --- /dev/null +++ b/tests/Utils/SignUpConfig.php @@ -0,0 +1,47 @@ +set('registration_enabled', true); + $config->set('enable_password', true); + $config->set('enable_pronoun', true); + $config->set('goodie_type', GoodieType::Tshirt->value); + $config->set('tshirt_sizes', [ + 'S' => 'Small Straight-Cut', + 'M' => 'Medium Straight-Cut', + ]); + // disallow numeric values in username for tests + $config->set('username_regex', '/\d+/'); + $config->set('min_password_length', 3); + $config->set('theme', 0); + $config->set('enable_planned_arrival', true); + $config->set('enable_user_name', true); + $config->set('enable_mobile_show', true); + $config->set('enable_dect', true); + } + + public static function setMinimumConfig(Config $config): void + { + $config->set('registration_enabled', true); + $config->set('enable_password', true); + $config->set('enable_pronoun', false); + $config->set('goodie_type', GoodieType::None->value); + // disallow numeric values in username for tests + $config->set('username_regex', '/\d+/'); + $config->set('min_password_length', 3); + $config->set('theme', 0); + $config->set('enable_planned_arrival', false); + $config->set('enable_user_name', false); + $config->set('enable_mobile_show', false); + $config->set('enable_dect', false); + } +}