Implement new sign up page

This commit is contained in:
Michael Weimann 2023-04-01 14:23:52 +02:00 committed by Igor Scheller
parent c2dd25fc7c
commit 4329ee4af9
30 changed files with 2126 additions and 591 deletions

View File

@ -8,6 +8,8 @@ use FastRoute\RouteCollector;
// Pages // Pages
$route->get('/', 'HomeController@index'); $route->get('/', 'HomeController@index');
$route->get('/sign-up', 'SignUpController@view');
$route->post('/sign-up', 'SignUpController@save');
$route->get('/credits', 'CreditsController@index'); $route->get('/credits', 'CreditsController@index');
$route->get('/health', 'HealthController@index'); $route->get('/health', 'HealthController@index');

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Database\Factories\Engelsystem\Models;
use Engelsystem\Models\OAuth;
use Illuminate\Database\Eloquent\Factories\Factory;
class OAuthFactory extends Factory
{
/** @var class-string */
protected $model = OAuth::class; // phpcs:ignore
/**
* @return array<string, mixed>
*/
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',
];
}
}

View File

@ -62,7 +62,6 @@ $includeFiles = [
__DIR__ . '/../includes/pages/admin_groups.php', __DIR__ . '/../includes/pages/admin_groups.php',
__DIR__ . '/../includes/pages/admin_shifts.php', __DIR__ . '/../includes/pages/admin_shifts.php',
__DIR__ . '/../includes/pages/admin_user.php', __DIR__ . '/../includes/pages/admin_user.php',
__DIR__ . '/../includes/pages/guest_login.php',
__DIR__ . '/../includes/pages/user_myshifts.php', __DIR__ . '/../includes/pages/user_myshifts.php',
__DIR__ . '/../includes/pages/user_shifts.php', __DIR__ . '/../includes/pages/user_shifts.php',

View File

@ -1,543 +0,0 @@
<?php
use Carbon\Carbon;
use Engelsystem\Database\Database;
use Engelsystem\Events\Listener\OAuth2;
use Engelsystem\Config\GoodieType;
use Engelsystem\Http\Validation\Rules\Username;
use Engelsystem\Models\AngelType;
use Engelsystem\Models\Group;
use Engelsystem\Models\OAuth;
use Engelsystem\Models\User\Contact;
use Engelsystem\Models\User\PersonalData;
use Engelsystem\Models\User\Settings;
use Engelsystem\Models\User\State;
use Engelsystem\Models\User\User;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Collection;
/**
* @return string
*/
function register_title()
{
return __('Register');
}
/**
* Engel registrieren
*
* @return string
*/
function guest_register()
{
$authUser = auth()->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 <a href="mailto:%s">%1$s</a>.', [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(
' (<a href="%s">%s</a>)',
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');
}

View File

@ -107,6 +107,10 @@ table,
} }
} }
.list-group .form-check-input {
border-color: $list-group-form-check-input-border-color;
}
// Navs ======================================================================= // Navs =======================================================================
.nav-tabs, .nav-tabs,

View File

@ -150,6 +150,9 @@ $input-border-focus: #66afe9 !default;
//** Placeholder text color //** Placeholder text color
$input-color-placeholder: $gray-light !default; $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-color: $text-color !default;
$legend-border-color: $gray-dark !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` //** Background color on `.list-group-item`
$list-group-bg: $gray-darker !default; $list-group-bg: $gray-darker !default;
//** `.list-group-item` border color //** `.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
$list-group-border-radius: $border-radius-base !default; $list-group-border-radius: $border-radius-base !default;

View File

@ -4,6 +4,7 @@ $input-disabled-bg: #111;
$alert-bg-scale: 70%; $alert-bg-scale: 70%;
$secondary: #222; $secondary: #222;
$table-striped-bg: rgba(#fff, 0.05); $table-striped-bg: rgba(#fff, 0.05);
$list-group-form-check-input-border-color: #999;
$es-choices-highlight-color: #000; $es-choices-highlight-color: #000;

View File

@ -17,19 +17,45 @@ msgstr ""
msgid "validation.password.required" msgid "validation.password.required"
msgstr "Bitte gib ein Passwort an." msgstr "Bitte gib ein Passwort an."
msgid "validation.password.length"
msgstr "Das angegebene Passwort ist zu kurz."
msgid "validation.login.required" msgid "validation.login.required"
msgstr "Bitte gib einen Loginnamen an." 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" msgid "validation.email.required"
msgstr "Bitte gib eine E-Mail-Adresse an." msgstr "Bitte gib eine E-Mail-Adresse an."
msgid "validation.email.email" msgid "validation.email.email"
msgstr "Die E-Mail-Adresse ist nicht gültig." msgstr "Die E-Mail-Adresse ist nicht gültig."
msgid "validation.password.min" msgid "validation.password.length"
msgstr "Dein neues Passwort ist zu kurz." msgstr "Dein neues Passwort ist zu kurz."
msgid "validation.new_password.min" msgid "validation.new_password.length"
msgstr "Dein neues Passwort ist zu kurz." msgstr "Dein neues Passwort ist zu kurz."
msgid "validation.password.confirmed" msgid "validation.password.confirmed"
@ -38,6 +64,21 @@ msgstr "Deine Passwörter stimmen nicht überein."
msgid "validation.password_confirmation.required" msgid "validation.password_confirmation.required"
msgstr "Du musst dein Passwort bestätigen." 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" msgid "schedule.edit.success"
msgstr "Das Programm wurde erfolgreich konfiguriert." msgstr "Das Programm wurde erfolgreich konfiguriert."

View File

@ -1020,15 +1020,6 @@ msgstr ""
msgid "Edit user" msgid "Edit user"
msgstr "User bearbeiten" 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." msgid "Please enter a nickname."
msgstr "Gib bitte einen Nick an." msgstr "Gib bitte einen Nick an."
@ -1067,9 +1058,6 @@ msgstr ""
"Mit diesem Formular registrierst Du Dich als Engel. Du bekommst ein Konto in " "Mit diesem Formular registrierst Du Dich als Engel. Du bekommst ein Konto in "
"der Engel-Aufgabenverwaltung." "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." msgid "Allow heaven angels to contact you by e-mail."
msgstr "Erlaube Himmel-Engeln dich per Mail zu kontaktieren." 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 <a href=\"mailto:%s\">%1$s</a>." msgid "To withdraw your approval, send an email to <a href=\"mailto:%s\">%1$s</a>."
msgstr "Dies kann jederzeit durch eine E-Mail an <a href=\"mailto:%s\">%1$s</a> widerrufen werden." msgstr "Dies kann jederzeit durch eine E-Mail an <a href=\"mailto:%s\">%1$s</a> 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" msgid "Mobile"
msgstr "Handy" msgstr "Handy"
@ -2134,6 +2103,9 @@ msgstr "Pflichtfeld"
msgid "settings.profile.nick" msgid "settings.profile.nick"
msgstr "Nick" msgstr "Nick"
msgid "settings.profile.nick.already-taken"
msgstr "Der Nick ist bereits vergeben."
msgid "settings.profile.pronoun" msgid "settings.profile.pronoun"
msgstr "Pronomen" msgstr "Pronomen"
@ -2161,9 +2133,15 @@ msgstr "Handy"
msgid "settings.profile.mobile_show" msgid "settings.profile.mobile_show"
msgstr "Mache meine Handynummer für andere Benutzer sichtbar." msgstr "Mache meine Handynummer für andere Benutzer sichtbar."
msgid "settings.profile.email-preferences"
msgstr "E-Mail Einstellungen"
msgid "settings.profile.email" msgid "settings.profile.email"
msgstr "E-Mail" msgstr "E-Mail"
msgid "settings.profile.email.already-taken"
msgstr "Die E-Mail-Adresse ist bereits vergeben."
msgid "settings.profile.email_shiftinfo" msgid "settings.profile.email_shiftinfo"
msgstr "Das %s darf mir E-Mails senden (z.B. wenn sich meine Schichten ändern)." 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" msgid "settings.password.info"
msgstr "Hier kannst Du Dein Passwort ändern." 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" msgid "settings.password.password"
msgstr "Altes Passwort" msgstr "Altes Passwort"
@ -2412,6 +2393,9 @@ msgid "angeltypes.restricted.hint"
msgstr "Dieser Engeltyp benötigt eine Einweisung bei einem Einführungstreffen. " msgstr "Dieser Engeltyp benötigt eine Einweisung bei einem Einführungstreffen. "
"Weitere Informationen findest du möglicherweise in der Beschreibung." "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" msgid "angeltypes.name"
msgstr "Name" msgstr "Name"
@ -2456,3 +2440,27 @@ msgstr "Tag %1$d"
msgid "dashboard.day" msgid "dashboard.day"
msgstr "Tag" 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!"

View File

@ -15,19 +15,45 @@ msgstr "No user was found or password is wrong. Please try again. If you are sti
msgid "validation.password.required" msgid "validation.password.required"
msgstr "The password is required." msgstr "The password is required."
msgid "validation.password.length"
msgstr "The password entered is too short."
msgid "validation.login.required" msgid "validation.login.required"
msgstr "The login name is 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" msgid "validation.email.required"
msgstr "The email address is required." msgstr "The email address is required."
msgid "validation.email.email" msgid "validation.email.email"
msgstr "This email address is not valid." msgstr "This email address is not valid."
msgid "validation.password.min" msgid "validation.password.length"
msgstr "Your new password is too short." msgstr "Your new password is too short."
msgid "validation.new_password.min" msgid "validation.new_password.length"
msgstr "Your new password is too short." msgstr "Your new password is too short."
msgid "validation.password.confirmed" msgid "validation.password.confirmed"
@ -36,6 +62,21 @@ msgstr "Your passwords are not equal."
msgid "validation.password_confirmation.required" msgid "validation.password_confirmation.required"
msgstr "You have to confirm your password." 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" msgid "schedule.edit.success"
msgstr "The schedule was configured successfully." msgstr "The schedule was configured successfully."

View File

@ -229,6 +229,9 @@ msgstr "Entry required"
msgid "settings.profile.nick" msgid "settings.profile.nick"
msgstr "Nick" msgstr "Nick"
msgid "settings.profile.nick.already-taken"
msgstr "The nick is already taken."
msgid "settings.profile.pronoun" msgid "settings.profile.pronoun"
msgstr "Pronoun" msgstr "Pronoun"
@ -256,9 +259,15 @@ msgstr "Mobile"
msgid "settings.profile.mobile_show" msgid "settings.profile.mobile_show"
msgstr "Show mobile number to other users to contact me." msgstr "Show mobile number to other users to contact me."
msgid "settings.profile.email-preferences"
msgstr "E-Mail preferences"
msgid "settings.profile.email" msgid "settings.profile.email"
msgstr "E-Mail" msgstr "E-Mail"
msgid "settings.profile.email.already-taken"
msgstr "The email is already taken."
msgid "settings.profile.email_shiftinfo" msgid "settings.profile.email_shiftinfo"
msgstr "The %s is allowed to send me an e-mail (e.g. when my shifts change)." 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" msgid "settings.password.info"
msgstr "Here you can change your password." 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" msgid "settings.password.password"
msgstr "Old password" msgstr "Old password"
@ -507,6 +519,9 @@ msgid "angeltypes.restricted.hint"
msgstr "This angeltype requires the attendance at an introduction meeting. " msgstr "This angeltype requires the attendance at an introduction meeting. "
"You might find additional information in the description." "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" msgid "angeltypes.name"
msgstr "Name" msgstr "Name"
@ -551,3 +566,27 @@ msgstr "Day %1$d"
msgid "dashboard.day" msgid "dashboard.day"
msgstr "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."

View File

@ -59,7 +59,7 @@
{% include "layouts/parts/language_dropdown.twig" %} {% include "layouts/parts/language_dropdown.twig" %}
{% if has_permission_to('register') and config('registration_enabled') %} {% 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 %} {% endif %}
{% if has_permission_to('login') %} {% if has_permission_to('login') %}

View File

@ -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.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 {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.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) %} {% macro checkbox(name, label, opt) %}
<div class="form-check mb-3"> <div class="form-check {{ opt.class is defined ? opt.class : 'mb-3' }}">
<input class="form-check-input" type="checkbox" id="{{ name }}" name="{{ name }}" <input class="form-check-input" type="checkbox" id="{{ name }}" name="{{ name }}"
value="{{ opt.value|default('1') }}" value="{{ opt.value|default('1') }}"
{%- if opt.checked|default(false) %} checked{% endif %} {%- if opt.checked|default(false) %} checked{% endif %}

View File

@ -15,7 +15,7 @@
</a> </a>
{% else %} {% else %}
{% if has_permission_to('register') and config('registration_enabled') %} {% if has_permission_to('register') and config('registration_enabled') %}
<a href="{{ url('/register') }}" class="btn btn-secondary back"> <a href="{{ url('/sign-up') }}" class="btn btn-secondary back">
{{ __('registration.register') }} {{ __('registration.register') }}
</a> </a>
{% endif %} {% endif %}

View File

@ -106,7 +106,7 @@
{% if has_permission_to('register') and config('registration_enabled') %} {% if has_permission_to('register') and config('registration_enabled') %}
{% if config('enable_password') %} {% if config('enable_password') %}
<p>{{ __('Please sign up, if you want to help us!') }}</p> <p>{{ __('Please sign up, if you want to help us!') }}</p>
<a href="{{ url('register') }}" class="btn btn-primary">{{ __('Register') }} &raquo;</a> <a href="{{ url('/sign-up') }}" class="btn btn-primary">{{ __('Register') }} &raquo;</a>
{% else %} {% else %}
<p>{{ __('Registration is only available via external login.') }}</p> <p>{{ __('Registration is only available via external login.') }}</p>
{% endif %} {% endif %}

View File

@ -109,6 +109,7 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">{{ __('settings.profile.email-preferences') }}</label>
{{ f.checkbox('email_shiftinfo', __('settings.profile.email_shiftinfo', [config('app_name')]), { {{ f.checkbox('email_shiftinfo', __('settings.profile.email_shiftinfo', [config('app_name')]), {
'checked': user.settings.email_shiftinfo, 'checked': user.settings.email_shiftinfo,
}) }} }) }}

View File

@ -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 %}
<div class="container">
<div class="mb-5">
<h1>{{ __('page.sign-up.title') }}</h1>
</div>
{% include 'layouts/parts/messages.twig' %}
<form method="post" action="{{ url('/sign-up') }}" novalidate class="mb-5">
{{ csrf() }}
<div class="mb-5">
<h2>{{ __('page.sign-up.login-data') }}</h2>
<div class="row">
{% if isPronounEnabled %}
<div class="col-md-6">
{{ f.input(
'pronoun',
__('settings.profile.pronoun'),
{
'max_length': 15,
'value': f.formData('pronoun', ''),
}
) }}
</div>
{% endif %}
<div class="col-md-6">
{{ f.input(
'username',
__('settings.profile.nick'),
{
'autocomplete': 'nickname',
'max_length': 24,
'required': true,
'required_icon': true,
'value': f.formData('username', ''),
}
) }}
</div>
{% if not isPronounEnabled %}
{# Insert an empty row to keep password / email in line #}
<div class="col-md-6"></div>
{% endif %}
{% if isPasswordEnabled %}
<div class="col-md-6">
{{ f.input(
'password',
__('settings.password'),
{
'type': 'password',
'autocomplete': 'new-password',
'required': true,
'required_icon': true,
}
) }}
</div>
<div class="col-md-6">
{{ f.input(
"password_confirmation",
__('settings.password.new_password2'),
{
'type': 'password',
'autocomplete': 'new-password',
'required': true,
'required_icon': true,
}
) }}
</div>
{% endif %}
<div class="col-md-6">
{{ f.input(
'email',
__('settings.profile.email'),
{
'type': 'email',
'max_length': 254,
'required': true,
'required_icon': true,
'value': f.formData('email', ''),
}
) }}
</div>
<div class="col-md-6">
<label class="form-label">{{ __('settings.profile.email-preferences') }}</label>
<ul class="list-group">
<li class="list-group-item">
{# 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),
},
) }}
</li>
<li class="list-group-item">
{# Empty class to prevent the default bottom margin #}
{{ f.checkbox(
'email_news',
__('settings.profile.email_news'),
{
'class': '',
'checked': f.formData('email_news', false),
},
) }}
</li>
<li class="list-group-item">
{# Empty class to prevent the default bottom margin #}
{{ f.checkbox(
'email_messages',
__('settings.profile.email_messages'),
{
'class': '',
'checked': f.formData('email_messages', false),
},
) }}
</li>
<li class="list-group-item">
{# 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),
},
) }}
</li>
</ul>
</div>
</div>
</div>
{% if isFullNameEnabled %}
<div class="mb-5">
<h2>{{ __('page.sign-up.name') }}</h2>
<div class="row">
<div class="col-md-6">
{{ f.input(
'firstname',
__('settings.profile.firstname'),
{
'autocomplete': 'given-name',
'max_length': 64,
'value': f.formData('firstname', ''),
}
) }}
</div>
<div class="col-md-6">
{{ f.input(
'lastname',
__('settings.profile.lastname'),
{
'autocomplete': 'family-name',
'max_length': 64,
'value': f.formData('lastname', ''),
}
) }}
</div>
</div>
</div>
{% endif %}
<div class="mb-5">
<h2>{{ __('page.sign-up.event-data') }}</h2>
<div class="row">
{% if isGoodieEnabled %}
<div class="col-md-6">
{% 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),
},
) }}
</div>
<div class="col-md-6"></div>
{% endif %}
{% if isPlannedArrivalDateEnabled %}
<div class="col-md-6">
{{ 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),
}
) }}
</div>
{% endif %}
{% if isGoodieTShirt %}
<div class="col-md-6">
{{ f.select(
'tshirt_size',
__('settings.profile.shirt_size'),
tShirtSizes,
{
'default_option': __('form.select_placeholder'),
'required': true,
'required_icon': true,
'selected': f.formData('tshirt_size', ''),
}
) }}
</div>
{% endif %}
<div class="col-md-6">
{{ 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 %}
</div>
{% if isDECTEnabled %}
<div class="col-md-6">
{{ f.input(
"dect",
__("settings.profile.dect"),
{
'type': 'tel-local',
'max_length': 40,
'value': f.formData('dect', ''),
}
) }}
</div>
{% endif %}
</div>
</div>
<div class="mb-5">
<h2>{{ __('page.sign-up.what-do-you-want-to-do') }}</h2>
<div class="row mb-3">
{% for angelType in angelTypes %}
<div class="col-sm-6 col-md-4 col-lg-3 col-xl-2">
{{ 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,
}
) }}
</div>
{% endfor %}
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-2">
{{ m.icon('mortarboard-fill', 'text-body') }}
{{ __('angeltypes.restricted.hint') }}
</div>
<div>
{{ m.icon('info-circle', 'text-body') }}
{{ __('angeltypes.can-change-later') }}
</div>
</div>
</div>
</div>
{#
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',
}) }}
</form>
</div>
{% endblock %}

View File

@ -328,6 +328,6 @@ class OAuthController extends BaseController
$this->session->set('oauth2_enable_password', $config['enable_password']); $this->session->set('oauth2_enable_password', $config['enable_password']);
$this->session->set('oauth2_allow_registration', $config['allow_registration']); $this->session->set('oauth2_allow_registration', $config['allow_registration']);
return $this->redirect->to('/register'); return $this->redirect->to('/sign-up');
} }
} }

View File

@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Controllers;
use Engelsystem\Config\Config;
use Engelsystem\Config\GoodieType;
use Engelsystem\Events\Listener\OAuth2;
use Engelsystem\Factories\User;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Redirector;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Models\AngelType;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class SignUpController extends BaseController
{
use HasUserNotifications;
public function __construct(
private Config $config,
private Response $response,
private Redirector $redirect,
private SessionInterface $session,
private Authenticator $auth,
private OAuth2 $oAuth,
private User $userFactory
) {
}
public function view(): Response
{
if ($this->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<string, 1> 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<string, 1>
*/
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<string, 1>
*/
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);
}
}

337
src/Factories/User.php Normal file
View File

@ -0,0 +1,337 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Factories;
use Carbon\CarbonImmutable;
use DateTimeInterface;
use Engelsystem\Config\Config;
use Engelsystem\Config\GoodieType;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Helpers\Carbon;
use Engelsystem\Http\Exceptions\ValidationException;
use Engelsystem\Http\Validation\Validator;
use Engelsystem\Models\AngelType;
use Engelsystem\Models\Group;
use Engelsystem\Models\OAuth;
use Engelsystem\Models\User\Contact;
use Engelsystem\Models\User\PersonalData;
use Engelsystem\Models\User\Settings;
use Engelsystem\Models\User\State;
use Engelsystem\Models\User\User as EngelsystemUser;
use Illuminate\Database\Connection;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class User
{
public function __construct(
private Authenticator $authenticator,
private Config $config,
private Connection $dbConnection,
private LoggerInterface $logger,
private SessionInterface $session,
private Validator $validator,
) {
}
/**
* Takes some arbitrary data, validates it and tries to create a user from it.
*
* @param Array<string, mixed> $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<string, mixed> $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<string, mixed> $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<string, mixed> $data
* @param Array<string, mixed> $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();
}
}

View File

@ -108,10 +108,6 @@ class LegacyMiddleware implements MiddlewareInterface
$title = shifts_title(); $title = shifts_title();
$content = user_shifts(); $content = user_shifts();
return [$title, $content]; return [$title, $content];
case 'register':
$title = register_title();
$content = guest_register();
return [$title, $content];
case 'admin_user': case 'admin_user':
$title = admin_user_title(); $title = admin_user_title();
$content = admin_user(); $content = admin_user();

View File

@ -6,6 +6,7 @@ namespace Engelsystem\Models;
use Carbon\Carbon; use Carbon\Carbon;
use Engelsystem\Models\User\UsesUserModel; use Engelsystem\Models\User\UsesUserModel;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\Query\Builder as QueryBuilder;
/** /**
@ -26,6 +27,7 @@ use Illuminate\Database\Query\Builder as QueryBuilder;
*/ */
class OAuth extends BaseModel class OAuth extends BaseModel
{ {
use HasFactory;
use UsesUserModel; use UsesUserModel;
public $table = 'oauth'; // phpcs:ignore public $table = 'oauth'; // phpcs:ignore

View File

@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Test\Feature\Controllers;
use Engelsystem\Application;
use Engelsystem\Config\Config;
use Engelsystem\Controllers\SignUpController;
use Engelsystem\Events\Listener\OAuth2;
use Engelsystem\Models\AngelType;
use Engelsystem\Models\BaseModel;
use Engelsystem\Test\Feature\ApplicationFeatureTest;
use Engelsystem\Test\Utils\FormFieldAssert;
use Engelsystem\Test\Utils\SignUpConfig;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* @group sign-up-controller-tests
*/
final class SignUpControllerTest extends ApplicationFeatureTest
{
private Application $application;
private Config $config;
private SessionInterface $session;
/**
* @var OAuth2&MockObject
*/
private OAuth2 $oauth;
/**
* @var Array<BaseModel>
*/
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<AngelType>
*/
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();
}
}
}

View File

@ -17,6 +17,7 @@ use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\Test\TestLogger; use Psr\Log\Test\TestLogger;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
abstract class ControllerTest extends TestCase abstract class ControllerTest extends TestCase
@ -78,6 +79,7 @@ abstract class ControllerTest extends TestCase
$this->session = new Session(new MockArraySessionStorage()); $this->session = new Session(new MockArraySessionStorage());
$this->app->instance('session', $this->session); $this->app->instance('session', $this->session);
$this->app->instance(Session::class, $this->session); $this->app->instance(Session::class, $this->session);
$this->app->instance(SessionInterface::class, $this->session);
$this->app->bind(UrlGeneratorInterface::class, UrlGenerator::class); $this->app->bind(UrlGeneratorInterface::class, UrlGenerator::class);

View File

@ -440,7 +440,7 @@ class OAuthControllerTest extends TestCase
$this->setExpects($this->auth, 'user', null, null, $this->atLeastOnce()); $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 = new Request();
$request = $request $request = $request
@ -533,7 +533,7 @@ class OAuthControllerTest extends TestCase
$this->setExpects($this->auth, 'user', null, null, $this->atLeastOnce()); $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 = new Request();
$request = $request $request = $request

View File

@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Test\Unit\Controllers;
use Engelsystem\Controllers\SignUpController;
use Engelsystem\Factories\User;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Models\OAuth;
use Engelsystem\Models\User\User as EngelsystemUser;
use PHPUnit\Framework\MockObject\MockObject;
/**
* @group sign-up-controller-tests
*/
final class SignUpControllerTest extends ControllerTest
{
/**
* @var Authenticator&MockObject
*/
private Authenticator $authenticator;
/**
* @var MockObject&User
*/
private User $userFactory;
private SignUpController $subject;
public function setUp(): void
{
parent::setUp();
$this->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);
}
}

View File

@ -0,0 +1,496 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Test\Unit\Factories;
use Carbon\Carbon;
use Carbon\CarbonImmutable;
use Engelsystem\Config\Config;
use Engelsystem\Factories\User;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Exceptions\ValidationException;
use Engelsystem\Http\Request;
use Engelsystem\Models\AngelType;
use Engelsystem\Models\User\User as UserModel;
use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\ServiceProviderTest;
use Engelsystem\Test\Utils\SignUpConfig;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
class UserTest extends ServiceProviderTest
{
use HasDatabase;
private User $subject;
private Config $config;
private SessionInterface $session;
private CarbonImmutable $now;
public function setUp(): void
{
parent::setUp();
$this->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<AngelType>
*/
private function createAngelTypes(): array
{
return [
AngelType::create([
'name' => 'Test angel type 1',
]),
AngelType::create([
'name' => 'Test angel type 2',
'hide_register' => true,
]),
];
}
/**
* @param Array<string, mixed> $data Data passed to User::createFromData
* @param Array<string, Array<string>> $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);
}
}
}

View File

@ -9,6 +9,7 @@ use Engelsystem\Database\Migration\Migrate;
use Engelsystem\Database\Migration\MigrationServiceProvider; use Engelsystem\Database\Migration\MigrationServiceProvider;
use Engelsystem\Http\Request; use Engelsystem\Http\Request;
use Illuminate\Database\Capsule\Manager as CapsuleManager; use Illuminate\Database\Capsule\Manager as CapsuleManager;
use Illuminate\Database\Connection;
use PDO; use PDO;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
@ -30,6 +31,7 @@ trait HasDatabase
$this->database = new Database($connection); $this->database = new Database($connection);
$this->app->instance(Database::class, $this->database); $this->app->instance(Database::class, $this->database);
$this->app->instance(Connection::class, $connection);
$this->app->register(MigrationServiceProvider::class); $this->app->register(MigrationServiceProvider::class);
$this->app->instance(ServerRequestInterface::class, new Request()); $this->app->instance(ServerRequestInterface::class, new Request());

View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Test\Utils;
use PHPUnit\Framework\Assert;
/**
* Class that provides some form field assertions.
*/
final class FormFieldAssert
{
/**
* Asserts that the HTML does contain an INPUT field with the given name.
*/
public static function assertContainsInputField(string $name, string $html): void
{
Assert::assertMatchesRegularExpression(self::makeInputPattern('input', $name), $html);
}
/**
* Asserts that the HTML does not contain an INPUT field with the given name.
*/
public static function assertNotContainsInputField(string $name, string $html): void
{
Assert::assertDoesNotMatchRegularExpression(self::makeInputPattern('input', $name), $html);
}
/**
* Asserts that the HTML does contain a SELECT field with the given name.
*/
public static function assertContainsSelectField(string $name, string $html): void
{
Assert::assertMatchesRegularExpression(self::makeInputPattern('select', $name), $html);
}
/**
* Asserts that the HTML does not contain a SELECT field with the given name.
*/
public static function assertNotContainsSelectField(string $name, string $html): void
{
Assert::assertDoesNotMatchRegularExpression(self::makeInputPattern('select', $name), $html);
}
/**
* Asserts that the HTML does contain an INPUT field of the type "checkbox" with the give name and
* with the "checked" attribute.
*/
public static function assertContainsCheckedCheckbox(string $name, string $html): void
{
Assert::assertMatchesRegularExpression(self::makeCheckedCheckboxPattern($name), $html);
}
/**
* Asserts that the HTML does contain an INPUT field of the type "checkbox" with the given name and
* without the "checked" attribute.
*/
public static function assertContainsUncheckedCheckbox(string $name, string $html): void
{
self::assertContainsInputField($name, $html);
Assert::assertDoesNotMatchRegularExpression(self::makeCheckedCheckboxPattern($name), $html);
}
private static function makeInputPattern(string $tag, string $name): string
{
return strtr('/<$TAG[^>]*name="$NAME"/s', [
'$TAG' => $tag,
'$NAME' => $name,
]);
}
private static function makeCheckedCheckboxPattern(string $name): string
{
return strtr('/<input[^>]*type="checkbox"[^>]*name="$NAME"[^>]*checked.*?/s', [
'$NAME' => $name,
]);
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Test\Utils;
use Engelsystem\Config\Config;
use Engelsystem\Config\GoodieType;
final class SignUpConfig
{
public static function setMaximumConfig(Config $config): void
{
$config->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);
}
}