Merge pull request #280 from nextcloud/feature/noid/more-profile-fields

Feature/noid/more profile fields
This commit is contained in:
Joas Schilling 2021-04-01 13:48:49 +02:00 committed by GitHub
commit ddab69b207
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 9898 additions and 290 deletions

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
/js/*
/tests/*

5
.eslintrc.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
extends: [
'@nextcloud'
]
};

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
/js/registration-settings.js binary
/js/registration-settings.js.map binary

View File

@ -1,55 +0,0 @@
name: PHP AppCode Check
on:
pull_request:
push:
branches:
- master
- stable*
env:
APP_NAME: registration
jobs:
unit-tests:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['7.4']
server-versions: ['stable20', 'stable21', 'master']
name: AppCode check php${{ matrix.php-versions }}-${{ matrix.server-versions }}
steps:
- name: Checkout server
uses: actions/checkout@v2
with:
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout submodules
shell: bash
run: |
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
git submodule sync --recursive
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
- name: Checkout app
uses: actions/checkout@v2
with:
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
tools: phpunit
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite
- name: Checkout app
uses: actions/checkout@v2
with:
path: apps/${{ env.APP_NAME }}
- name: App code check
run: php occ app:check-code ${{ env.APP_NAME }}

View File

@ -0,0 +1,21 @@
name: Dependabot
on: pull_request_target
jobs:
auto-merge:
runs-on: ubuntu-latest
steps:
# Default github action approve
- uses: hmarr/auto-approve-action@v2.0.0
if: github.ref == 'refs/heads/master' &&
(github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]')
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# Nextcloud bot approve and merge request
- uses: ahmadnassri/action-dependabot-auto-merge@v2
if: github.ref == 'refs/heads/master' &&
(github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]')
with:
target: minor
github-token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN }}

View File

@ -52,6 +52,50 @@ jobs:
- name: Run coding standards check - name: Run coding standards check
run: composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 ) run: composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 )
node:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
name: eslint node${{ matrix.node-version }}
steps:
- uses: actions/checkout@v2
- name: Set up node ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
stylelint:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
name: stylelint node${{ matrix.node-version }}
steps:
- uses: actions/checkout@v2
- name: Set up node ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run stylelint
xml-linters: xml-linters:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

34
.github/workflows/node.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: Node
on:
pull_request:
push:
branches:
- master
- stable*
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
name: node${{ matrix.node-version }}
steps:
- uses: actions/checkout@v2
- name: Set up node ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies & build
run: |
npm ci
npm run build --if-present
- name: Check webpack build changes
run: |
bash -c "[[ ! \"`git status --porcelain `\" ]] || ( echo 'Uncommited changes in webpack build' && git status && exit 1 )"

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
/.php_cs.cache /.php_cs.cache
/build /build
/node_modules
/tests/clover.xml /tests/clover.xml
/tests/coverage-html /tests/coverage-html
/tests/.phpunit.result.cache /tests/.phpunit.result.cache

3
.l10nignore Normal file
View File

@ -0,0 +1,3 @@
# compiled vue templates
js/registration-settings.js
js/registration-settings.js.map

View File

@ -57,6 +57,7 @@ appstore:
--exclude=tests \ --exclude=tests \
--exclude=vendor \ --exclude=vendor \
--exclude=webpack.*.js \ --exclude=webpack.*.js \
--exclude=webpack.js \
$(project_dir)/ $(sign_dir)/$(app_name) $(project_dir)/ $(sign_dir)/$(app_name)
@if [ -f $(cert_dir)/$(app_name).key ]; then \ @if [ -f $(cert_dir)/$(app_name).key ]; then \
echo "Signing app files…"; \ echo "Signing app files…"; \

3
babel.config.js Normal file
View File

@ -0,0 +1,3 @@
const babelConfig = require('@nextcloud/babel-config')
module.exports = babelConfig

View File

@ -1,13 +1,17 @@
#body-login #email, #body-login #email,
#body-login #token, #body-login #token,
#body-login #username, #body-login #loginname,
#body-login #fullname,
#body-login #phone,
#body-login #password { #body-login #password {
width: calc(100% - 56px); width: calc(100% - 56px);
padding-left: 36px; padding-left: 36px;
} }
#email-icon, #email-icon,
#token-icon, #token-icon,
#username-icon, #loginname-icon,
#fullname-icon,
#phone-icon,
#password-icon { #password-icon {
position: absolute; position: absolute;
left: 16px; left: 16px;
@ -16,8 +20,8 @@
filter: alpha(opacity=30); filter: alpha(opacity=30);
opacity: .3; opacity: .3;
} }
#username-icon { #email-icon {
top: 17px; top: 27px;
} }
input[type="submit"] { input[type="submit"] {
@ -41,3 +45,7 @@ input[type="submit"] {
.groupofone { .groupofone {
position: relative; position: relative;
} }
.toggle-password {
top: 22px !important;
}

377
js/registration-settings.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,23 +0,0 @@
$(document).ready(function() {
function saveSettings() {
OC.msg.startSaving('#registration_settings_msg');
$.ajax({
url: OC.generateUrl('/apps/registration/settings'),
type: 'POST',
data: $('#registration_settings_form').serialize(),
success: function(data){
OC.msg.finishedSaving('#registration_settings_msg', data);
},
error: function(data){
OC.msg.finishedError('#registration_settings_msg', data.responseJSON.message);
}
});
}
$('#registration_settings_form').change(saveSettings);
$('#registration').keypress(function(event) {
if (event.keyCode === 13) {
event.preventDefault();
}
});
});

View File

@ -99,7 +99,8 @@ class RegisterController extends Controller {
$params = [ $params = [
'email' => $email, 'email' => $email,
'message' => $message ?: $emailHint, 'message' => $message ?: $emailHint,
'disable_email_verification' => $this->config->getAppValue($this->appName, 'disable_email_verification', 'no') 'disable_email_verification' => $this->config->getAppValue($this->appName, 'disable_email_verification', 'no'),
'is_login_flow' => $this->loginFlowService->isUsingLoginFlow(),
]; ];
return new TemplateResponse('registration', 'form/email', $params, 'guest'); return new TemplateResponse('registration', 'form/email', $params, 'guest');
} }
@ -214,11 +215,13 @@ class RegisterController extends Controller {
* *
* @param string $secret * @param string $secret
* @param string $token * @param string $token
* @param string $username * @param string $loginname
* @param string $fullname
* @param string $phone
* @param string $message * @param string $message
* @return TemplateResponse * @return TemplateResponse
*/ */
public function showUserForm(string $secret, string $token, string $username = '', string $message = ''): TemplateResponse { public function showUserForm(string $secret, string $token, string $loginname = '', string $fullname = '', string $phone = '', string $password = '', string $message = ''): TemplateResponse {
try { try {
$registration = $this->validateSecretAndToken($secret, $token); $registration = $this->validateSecretAndToken($secret, $token);
} catch (RegistrationException $e) { } catch (RegistrationException $e) {
@ -230,8 +233,13 @@ class RegisterController extends Controller {
return new TemplateResponse('registration', 'form/user', [ return new TemplateResponse('registration', 'form/user', [
'email' => $registration->getEmail(), 'email' => $registration->getEmail(),
'email_is_login' => $this->config->getAppValue('registration', 'email_is_login', 'no') === 'yes', 'email_is_login' => $this->config->getAppValue('registration', 'email_is_login', 'no') === 'yes',
'username' => $username, 'loginname' => $loginname,
'fullname' => $fullname,
'show_fullname' => $this->config->getAppValue('registration', 'show_fullname', 'no') === 'yes',
'phone' => $phone,
'show_phone' => $this->config->getAppValue('registration', 'show_phone', 'no') === 'yes',
'message' => $message, 'message' => $message,
'password' => $password,
'additional_hint' => $additional_hint, 'additional_hint' => $additional_hint,
], 'guest'); ], 'guest');
} }
@ -243,11 +251,13 @@ class RegisterController extends Controller {
* *
* @param string $secret * @param string $secret
* @param string $token * @param string $token
* @param string $username * @param string $loginname
* @param string $fullname
* @param string $phone
* @param string $password * @param string $password
* @return RedirectResponse|TemplateResponse * @return RedirectResponse|TemplateResponse
*/ */
public function submitUserForm(string $secret, string $token, string $username, string $password): Response { public function submitUserForm(string $secret, string $token, string $loginname, string $fullname, string $phone, string $password): Response {
try { try {
$registration = $this->validateSecretAndToken($secret, $token); $registration = $this->validateSecretAndToken($secret, $token);
} catch (RegistrationException $e) { } catch (RegistrationException $e) {
@ -255,13 +265,13 @@ class RegisterController extends Controller {
} }
if ($this->config->getAppValue('registration', 'email_is_login', 'no') === 'yes') { if ($this->config->getAppValue('registration', 'email_is_login', 'no') === 'yes') {
$username = $registration->getEmail(); $loginname = $registration->getEmail();
} }
try { try {
$user = $this->registrationService->createAccount($registration, $username, $password); $user = $this->registrationService->createAccount($registration, $loginname, $fullname, $phone, $password);
} catch (Exception $exception) { } catch (Exception $exception) {
return $this->showUserForm($secret, $token, $username, $exception->getMessage()); return $this->showUserForm($secret, $token, $loginname, $fullname, $phone, $password, $exception->getMessage());
} }
// Delete registration // Delete registration
@ -314,7 +324,7 @@ class RegisterController extends Controller {
protected function validateSecretAndTokenErrorPage(): TemplateResponse { protected function validateSecretAndTokenErrorPage(): TemplateResponse {
return new TemplateResponse('core', 'error', [ return new TemplateResponse('core', 'error', [
'errors' => [ 'errors' => [
$this->l10n->t('The verification failed.'), ['error' => $this->l10n->t('The verification failed.')],
], ],
], 'error'); ], 'error');
} }

View File

@ -12,6 +12,7 @@
namespace OCA\Registration\Controller; namespace OCA\Registration\Controller;
use OCP\IGroup;
use \OCP\IRequest; use \OCP\IRequest;
use \OCP\AppFramework\Http\DataResponse; use \OCP\AppFramework\Http\DataResponse;
use \OCP\AppFramework\Http; use \OCP\AppFramework\Http;
@ -42,7 +43,7 @@ class SettingsController extends Controller {
/** /**
* @AdminRequired * @AdminRequired
* *
* @param string $registered_user_group all newly registered user will be put in this group * @param string|null $registered_user_group all newly registered user will be put in this group
* @param string $allowed_domains Registrations are only allowed for E-Mailadresses with these domains * @param string $allowed_domains Registrations are only allowed for E-Mailadresses with these domains
* @param string $additional_hint show Text at user-creation form * @param string $additional_hint show Text at user-creation form
* @param string $email_verification_hint if filled embed Text in Verification mail send to user * @param string $email_verification_hint if filled embed Text in Verification mail send to user
@ -53,13 +54,17 @@ class SettingsController extends Controller {
* @param bool|null $show_domains should the email list be shown to the user or not * @param bool|null $show_domains should the email list be shown to the user or not
* @return DataResponse * @return DataResponse
*/ */
public function admin(string $registered_user_group, public function admin(?string $registered_user_group,
string $allowed_domains, string $allowed_domains,
string $additional_hint, string $additional_hint,
string $email_verification_hint, string $email_verification_hint,
string $username_policy_regex, string $username_policy_regex,
?bool $admin_approval_required, ?bool $admin_approval_required,
?bool $email_is_login, ?bool $email_is_login,
?bool $show_fullname,
?bool $enforce_fullname,
?bool $show_phone,
?bool $enforce_phone,
?bool $domains_is_blocklist, ?bool $domains_is_blocklist,
?bool $show_domains, ?bool $show_domains,
?bool $disable_email_verification) { ?bool $disable_email_verification) {
@ -100,17 +105,15 @@ class SettingsController extends Controller {
$this->config->setAppValue($this->appName, 'admin_approval_required', $admin_approval_required ? 'yes' : 'no'); $this->config->setAppValue($this->appName, 'admin_approval_required', $admin_approval_required ? 'yes' : 'no');
$this->config->setAppValue($this->appName, 'email_is_login', $email_is_login ? 'yes' : 'no'); $this->config->setAppValue($this->appName, 'email_is_login', $email_is_login ? 'yes' : 'no');
$this->config->setAppValue($this->appName, 'show_fullname', $show_fullname ? 'yes' : 'no');
$this->config->setAppValue($this->appName, 'enforce_fullname', $enforce_fullname ? 'yes' : 'no');
$this->config->setAppValue($this->appName, 'show_phone', $show_phone ? 'yes' : 'no');
$this->config->setAppValue($this->appName, 'enforce_phone', $enforce_phone ? 'yes' : 'no');
$this->config->setAppValue($this->appName, 'domains_is_blocklist', $domains_is_blocklist ? 'yes' : 'no'); $this->config->setAppValue($this->appName, 'domains_is_blocklist', $domains_is_blocklist ? 'yes' : 'no');
$this->config->setAppValue($this->appName, 'show_domains', $show_domains ? 'yes' : 'no'); $this->config->setAppValue($this->appName, 'show_domains', $show_domains ? 'yes' : 'no');
$this->config->setAppValue($this->appName, 'disable_email_verification', $disable_email_verification ? 'yes' : 'no'); $this->config->setAppValue($this->appName, 'disable_email_verification', $disable_email_verification ? 'yes' : 'no');
// handle groups if ($registered_user_group === null) {
$groups = $this->groupmanager->search('');
$group_id_list = [];
foreach ($groups as $group) {
$group_id_list[] = $group->getGid();
}
if ($registered_user_group === 'none') {
$this->config->deleteAppValue($this->appName, 'registered_user_group'); $this->config->deleteAppValue($this->appName, 'registered_user_group');
return new DataResponse([ return new DataResponse([
'data' => [ 'data' => [
@ -118,7 +121,10 @@ class SettingsController extends Controller {
], ],
'status' => 'success', 'status' => 'success',
]); ]);
} elseif (in_array($registered_user_group, $group_id_list)) { }
$group = $this->groupmanager->get($registered_user_group);
if ($group instanceof IGroup) {
$this->config->setAppValue($this->appName, 'registered_user_group', $registered_user_group); $this->config->setAppValue($this->appName, 'registered_user_group', $registered_user_group);
return new DataResponse([ return new DataResponse([
'data' => [ 'data' => [
@ -126,7 +132,8 @@ class SettingsController extends Controller {
], ],
'status' => 'success', 'status' => 'success',
]); ]);
} else { }
return new DataResponse([ return new DataResponse([
'data' => [ 'data' => [
'message' => $this->l10n->t('No such group'), 'message' => $this->l10n->t('No such group'),
@ -135,4 +142,3 @@ class SettingsController extends Controller {
], Http::STATUS_NOT_FOUND); ], Http::STATUS_NOT_FOUND);
} }
} }
}

View File

@ -50,6 +50,8 @@ class MailService {
private $l10n; private $l10n;
/** @var IGroupManager */ /** @var IGroupManager */
private $groupManager; private $groupManager;
/** @var LoginFlowService */
private $loginFlowService;
/** @var ILogger */ /** @var ILogger */
private $logger; private $logger;
/** @var IConfig */ /** @var IConfig */
@ -61,6 +63,7 @@ class MailService {
IL10N $l10n, IL10N $l10n,
IGroupManager $groupManager, IGroupManager $groupManager,
IConfig $config, IConfig $config,
LoginFlowService $loginFlowService,
ILogger $logger) { ILogger $logger) {
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
$this->mailer = $mailer; $this->mailer = $mailer;
@ -68,6 +71,7 @@ class MailService {
$this->defaults = $defaults; $this->defaults = $defaults;
$this->l10n = $l10n; $this->l10n = $l10n;
$this->groupManager = $groupManager; $this->groupManager = $groupManager;
$this->loginFlowService = $loginFlowService;
$this->logger = $logger; $this->logger = $logger;
} }
@ -103,10 +107,16 @@ class MailService {
$template->addHeading($this->l10n->t('Registration')); $template->addHeading($this->l10n->t('Registration'));
$body = $this->l10n->t('Email address verified, you can now complete your registration.'); $body = $this->l10n->t('Email address verified, you can now complete your registration.');
if (!$this->loginFlowService->isUsingLoginFlow()) {
$template->addBodyText( $template->addBodyText(
htmlspecialchars($body . ' ' . $this->l10n->t('Click the button below to continue.')), htmlspecialchars($body . ' ' . $this->l10n->t('Click the button below to continue.')),
$body $body
); );
} else {
$template->addBodyText(
$body
);
}
// if the parameter is set through the settings panel add to body text // if the parameter is set through the settings panel add to body text
$email_verification_hint = $this->config->getAppValue('registration', 'email_verification_hint'); $email_verification_hint = $this->config->getAppValue('registration', 'email_verification_hint');
@ -118,10 +128,12 @@ class MailService {
$this->l10n->t('Verification code: %s', $registration->getToken()) $this->l10n->t('Verification code: %s', $registration->getToken())
); );
if (!$this->loginFlowService->isUsingLoginFlow()) {
$template->addBodyButton( $template->addBodyButton(
$this->l10n->t('Continue registration'), $this->l10n->t('Continue registration'),
$link $link
); );
}
$template->addFooter(); $template->addFooter();
$from = Util::getDefaultEmailAddress('register'); $from = Util::getDefaultEmailAddress('register');

View File

@ -30,6 +30,10 @@ declare(strict_types=1);
namespace OCA\Registration\Service; namespace OCA\Registration\Service;
use InvalidArgumentException;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberUtil;
use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Exceptions\InvalidTokenException;
use OC\Authentication\Exceptions\PasswordlessTokenException; use OC\Authentication\Exceptions\PasswordlessTokenException;
use OC\Authentication\Token\IProvider; use OC\Authentication\Token\IProvider;
@ -37,6 +41,7 @@ use OC\Authentication\Token\IToken;
use OCA\Registration\AppInfo\Application; use OCA\Registration\AppInfo\Application;
use OCA\Registration\Db\Registration; use OCA\Registration\Db\Registration;
use OCA\Registration\Db\RegistrationMapper; use OCA\Registration\Db\RegistrationMapper;
use OCP\Accounts\IAccountManager;
use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\DoesNotExistException;
use OCP\ILogger; use OCP\ILogger;
use OCP\IRequest; use OCP\IRequest;
@ -66,6 +71,8 @@ class RegistrationService {
private $registrationMapper; private $registrationMapper;
/** @var IUserManager */ /** @var IUserManager */
private $userManager; private $userManager;
/** @var IAccountManager */
private $accountManager;
/** @var IConfig */ /** @var IConfig */
private $config; private $config;
/** @var IGroupManager */ /** @var IGroupManager */
@ -92,6 +99,7 @@ class RegistrationService {
IURLGenerator $urlGenerator, IURLGenerator $urlGenerator,
RegistrationMapper $registrationMapper, RegistrationMapper $registrationMapper,
IUserManager $userManager, IUserManager $userManager,
IAccountManager $accountManager,
IConfig $config, IConfig $config,
IGroupManager $groupManager, IGroupManager $groupManager,
ISecureRandom $random, ISecureRandom $random,
@ -108,6 +116,7 @@ class RegistrationService {
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
$this->registrationMapper = $registrationMapper; $this->registrationMapper = $registrationMapper;
$this->userManager = $userManager; $this->userManager = $userManager;
$this->accountManager = $accountManager;
$this->config = $config; $this->config = $config;
$this->groupManager = $groupManager; $this->groupManager = $groupManager;
$this->random = $random; $this->random = $random;
@ -229,17 +238,44 @@ class RegistrationService {
* @throws RegistrationException * @throws RegistrationException
*/ */
public function validateUsername(string $username): void { public function validateUsername(string $username): void {
if ($username === "") { if ($username === '') {
throw new RegistrationException($this->l10n->t('Please provide a valid user name.')); throw new RegistrationException($this->l10n->t('Please provide a valid login name.'));
} }
$regex = $this->config->getAppValue($this->appName, 'username_policy_regex', ''); $regex = $this->config->getAppValue($this->appName, 'username_policy_regex', '');
if ($regex && preg_match($regex, $username) === 0) { if ($regex && preg_match($regex, $username) === 0) {
throw new RegistrationException($this->l10n->t('Please provide a valid user name.')); throw new RegistrationException($this->l10n->t('Please provide a valid login name.'));
} }
if ($this->registrationMapper->usernameIsPending($username) || $this->userManager->get($username) !== null) { if ($this->registrationMapper->usernameIsPending($username) || $this->userManager->get($username) !== null) {
throw new RegistrationException($this->l10n->t('The username you have chosen already exists.')); throw new RegistrationException($this->l10n->t('The login name you have chosen already exists.'));
}
}
/**
* @param string $phone
* @throws RegistrationException
*/
public function validatePhoneNumber(string $phone): void {
$defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
if ($defaultRegion === '') {
// When no default region is set, only +49… numbers are valid
if (strpos($phone, '+') !== 0) {
throw new RegistrationException($this->l10n->t('The phone number needs to contain the country code.'));
}
$defaultRegion = 'EN';
}
$phoneUtil = PhoneNumberUtil::getInstance();
try {
$phoneNumber = $phoneUtil->parse($phone, $defaultRegion);
if (!$phoneNumber instanceof PhoneNumber || !$phoneUtil->isValidNumber($phoneNumber)) {
throw new RegistrationException($this->l10n->t('The phone number is invalid.'));
}
} catch (NumberParseException $e) {
throw new RegistrationException($this->l10n->t('The phone number is invalid.'));
} }
} }
@ -290,41 +326,57 @@ class RegistrationService {
} }
/** /**
* @param $registration * @param Registration $registration
* @param string|null $username * @param string|null $loginName
* @param string|null $fullName
* @param string|null $phone
* @param string|null $password * @param string|null $password
* @return IUser * @return IUser
* @throws RegistrationException|InvalidTokenException * @throws RegistrationException|InvalidArgumentException
*/ */
public function createAccount(Registration $registration, ?string $username = null, ?string $password = null): IUser { public function createAccount(Registration $registration, ?string $loginName = null, ?string $fullName = null, ?string $phone = null, ?string $password = null): IUser {
if ($password === null && $registration->getPassword() === null) { if ($loginName === null) {
$generatedPassword = $this->generateRandomDeviceToken(); $loginName = $registration->getUsername();
$registration->setPassword($this->crypto->encrypt($generatedPassword));
}
if ($username === null) {
$username = $registration->getUsername();
} }
if ($registration->getPassword() !== null) { if ($registration->getPassword() !== null) {
$password = $this->crypto->decrypt($registration->getPassword()); $password = $this->crypto->decrypt($registration->getPassword());
} }
$this->validateUsername($username); if (!$password) {
throw new RegistrationException($this->l10n->t('Please provide a password.'));
}
$this->validateUsername($loginName);
if ($this->config->getAppValue('registration', 'show_fullname', 'no') === 'yes'
&& $this->config->getAppValue('registration', 'enforce_fullname', 'no') === 'yes') {
$this->validateDisplayname($fullName);
}
if (class_exists(PhoneNumberUtil::class)
&& $this->config->getAppValue('registration', 'show_phone', 'no') === 'yes') {
if ($phone) {
$this->validatePhoneNumber($phone);
} elseif ($this->config->getAppValue('registration', 'enforce_phone', 'no') === 'yes') {
throw new RegistrationException($this->l10n->t('Please provide a valid phone number.'));
}
}
/* TODO /* TODO
* createUser tests username validity once, but validateUsername already checked it, * createUser tests username validity once, but validateUsername already checked it,
* but createUser doesn't check if there is a pending registration with that name * but createUser doesn't check if there is a pending registration with that name
* *
* And validateUsername will throw RegistrationException while * And validateUsername will throw RegistrationException while
* createUser throws \InvalidTokenException in NC, \Exception in OC * createUser throws \InvalidArgumentException
*/ */
$user = $this->userManager->createUser($username, $password); $user = $this->userManager->createUser($loginName, $password);
if ($user === false) { if ($user === false) {
throw new RegistrationException($this->l10n->t('Unable to create user, there are problems with the user backend.')); throw new RegistrationException($this->l10n->t('Unable to create user, there are problems with the user backend.'));
} }
$userId = $user->getUID(); $userId = $user->getUID();
// Set user email // Set user email
try { try {
$user->setEMailAddress($registration->getEmail()); $user->setEMailAddress($registration->getEmail());
@ -332,6 +384,26 @@ class RegistrationService {
throw new RegistrationException($this->l10n->t('Unable to set user email: ' . $e->getMessage())); throw new RegistrationException($this->l10n->t('Unable to set user email: ' . $e->getMessage()));
} }
// Set display name
if ($fullName && $this->config->getAppValue('registration', 'show_fullname', 'no') === 'yes') {
$user->setDisplayName($fullName);
}
// Set phone number in account data
if (method_exists($this->accountManager, 'updateAccount')
&& $phone
&& $this->config->getAppValue('registration', 'show_phone', 'no') === 'yes') {
$account = $this->accountManager->getAccount($user);
$property = $account->getProperty(IAccountManager::PROPERTY_PHONE);
$account->setProperty(
IAccountManager::PROPERTY_PHONE,
$phone,
$property->getScope(),
IAccountManager::NOT_VERIFIED
);
$this->accountManager->updateAccount($account);
}
// Add user to group // Add user to group
$registeredUserGroup = $this->config->getAppValue($this->appName, 'registered_user_group', 'none'); $registeredUserGroup = $this->config->getAppValue($this->appName, 'registered_user_group', 'none');
if ($registeredUserGroup !== 'none') { if ($registeredUserGroup !== 'none') {

View File

@ -25,9 +25,13 @@ declare(strict_types=1);
namespace OCA\Registration\Settings; namespace OCA\Registration\Settings;
use libphonenumber\PhoneNumberUtil;
use OCA\Registration\AppInfo\Application; use OCA\Registration\AppInfo\Application;
use OCP\Accounts\IAccountManager;
use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig; use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager; use OCP\IGroupManager;
use OCP\Settings\ISettings; use OCP\Settings\ISettings;
@ -36,53 +40,97 @@ class RegistrationSettings implements ISettings {
private $config; private $config;
/** @var IGroupManager */ /** @var IGroupManager */
private $groupManager; private $groupManager;
/** @var IAccountManager */
private $accountManager;
/** @var IInitialState */
private $initialState;
/** @var string */ /** @var string */
protected $appName; protected $appName;
public function __construct(string $appName, public function __construct(string $appName,
IConfig $config, IConfig $config,
IGroupManager $groupManager) { IGroupManager $groupManager,
IAccountManager $accountManager,
IInitialState $initialState) {
$this->appName = $appName; $this->appName = $appName;
$this->config = $config; $this->config = $config;
$this->groupManager = $groupManager; $this->groupManager = $groupManager;
$this->accountManager = $accountManager;
$this->initialState = $initialState;
} }
public function getForm(): TemplateResponse { public function getForm(): TemplateResponse {
// handle groups $this->initialState->provideInitialState(
$groups = $this->groupManager->search(''); 'registered_user_group',
$groupIds = []; $this->getGroupDetailArray($this->config->getAppValue($this->appName, 'registered_user_group', 'none'))
foreach ($groups as $group) { );
$groupIds[] = $group->getGid();
}
$assignedGroups = $this->config->getAppValue($this->appName, 'registered_user_group', 'none');
// handle additional hint $this->initialState->provideInitialState(
$additional_hint = $this->config->getAppValue($this->appName, 'additional_hint', ''); 'admin_approval_required',
$email_verification_hint = $this->config->getAppValue($this->appName, 'email_verification_hint', ''); $this->config->getAppValue($this->appName, 'admin_approval_required', 'no') === 'yes'
);
// handle domains $this->initialState->provideInitialState(
$allowedDomains = $this->config->getAppValue($this->appName, 'allowed_domains', ''); 'allowed_domains',
$this->config->getAppValue($this->appName, 'allowed_domains')
);
$this->initialState->provideInitialState(
'domains_is_blocklist',
$this->config->getAppValue($this->appName, 'domains_is_blocklist', 'no') === 'yes'
);
$this->initialState->provideInitialState(
'show_domains',
$this->config->getAppValue($this->appName, 'show_domains', 'no') === 'yes'
);
$this->initialState->provideInitialState(
'disable_email_verification',
$this->config->getAppValue($this->appName, 'disable_email_verification', 'no') === 'yes'
);
$username_policy_regex = $this->config->getAppValue($this->appName, 'username_policy_regex', ''); $this->initialState->provideInitialState(
$adminApprovalRequired = $this->config->getAppValue($this->appName, 'admin_approval_required', 'no'); 'email_is_login',
$emailIsLogin = $this->config->getAppValue($this->appName, 'email_is_login', 'no'); $this->config->getAppValue($this->appName, 'email_is_login', 'no') === 'yes'
$domainsIsBlocklist = $this->config->getAppValue($this->appName, 'domains_is_blocklist', 'no'); );
$showDomains = $this->config->getAppValue($this->appName, 'show_domains', 'no'); $this->initialState->provideInitialState(
$disableEmailVerification = $this->config->getAppValue($this->appName, 'disable_email_verification', 'no'); 'username_policy_regex',
$this->config->getAppValue($this->appName, 'username_policy_regex')
);
$this->initialState->provideInitialState(
'username_policy_regex',
$this->config->getAppValue($this->appName, 'username_policy_regex')
);
$this->initialState->provideInitialState(
'show_fullname',
$this->config->getAppValue($this->appName, 'show_fullname', 'no') === 'yes'
);
$this->initialState->provideInitialState(
'enforce_fullname',
$this->config->getAppValue($this->appName, 'enforce_fullname', 'no') === 'yes'
);
// FIXME Always true when Nextcloud 22 or 21.0.1 is minimum requirement
$this->initialState->provideInitialState(
'can_show_phone',
method_exists($this->accountManager, 'updateAccount') && class_exists(PhoneNumberUtil::class)
);
$this->initialState->provideInitialState(
'show_phone',
$this->config->getAppValue($this->appName, 'show_phone', 'no') === 'yes'
);
$this->initialState->provideInitialState(
'enforce_phone',
$this->config->getAppValue($this->appName, 'enforce_phone', 'no') === 'yes'
);
return new TemplateResponse('registration', 'admin', [ $this->initialState->provideInitialState(
'groups' => $groupIds, 'additional_hint',
'current' => $assignedGroups, $this->config->getAppValue($this->appName, 'additional_hint')
'additional_hint' => $additional_hint, );
'email_verification_hint' => $email_verification_hint, $this->initialState->provideInitialState(
'username_policy_regex' => $username_policy_regex, 'email_verification_hint',
'allowed' => $allowedDomains, $this->config->getAppValue($this->appName, 'email_verification_hint')
'approval_required' => $adminApprovalRequired, );
'email_is_login' => $emailIsLogin,
'domains_is_blocklist' => $domainsIsBlocklist, return new TemplateResponse('registration', 'admin', [], TemplateResponse::RENDER_AS_BLANK);
'show_domains' => $showDomains,
'disable_email_verification' => $disableEmailVerification,
], '');
} }
public function getSection(): string { public function getSection(): string {
@ -92,4 +140,16 @@ class RegistrationSettings implements ISettings {
public function getPriority(): int { public function getPriority(): int {
return 50; return 50;
} }
protected function getGroupDetailArray(string $gid): array {
$group = $this->groupManager->get($gid);
if ($group instanceof IGroup) {
return [
'id' => $group->getGID(),
'displayname' => $group->getDisplayName(),
];
}
return [];
}
} }

8536
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

55
package.json Normal file
View File

@ -0,0 +1,55 @@
{
"name": "registration",
"version": "1.0.0",
"description": "",
"license": "AGPL-3.0-or-later",
"main": "main.js",
"scripts": {
"build": "NODE_ENV=production webpack --progress --config webpack.js",
"dev": "NODE_ENV=development webpack --progress --config webpack.js",
"watch": "NODE_ENV=development webpack --progress --watch --config webpack.js",
"lint": "eslint --ext .js,.vue src",
"lint:fix": "eslint --ext .js,.vue src --fix",
"stylelint": "stylelint src",
"stylelint:fix": "stylelint src --fix"
},
"dependencies": {
"@nextcloud/axios": "^1.6.0",
"@nextcloud/dialogs": "^3.1.1",
"@nextcloud/initial-state": "^1.2.0",
"@nextcloud/router": "^1.2.0",
"@nextcloud/vue": "^3.8.0",
"debounce": "^1.2.1",
"vue": "^2.6.12"
},
"devDependencies": {
"@babel/eslint-parser": "^7.13.14",
"@nextcloud/babel-config": "^1.0.0-beta.0",
"@nextcloud/eslint-config": "^5.0.0",
"@nextcloud/eslint-plugin": "^2.0.0",
"@nextcloud/webpack-vue-config": "^4.0.1",
"babel-loader": "^8.2.2",
"css-loader": "^4.3.0",
"eslint": "^7.23.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^4.1.0",
"eslint-plugin-vue": "^7.8.0",
"eslint-webpack-plugin": "^2.5.3",
"node-polyfill-webpack-plugin": "^1.1.0",
"sass": "^1.32.8",
"sass-loader": "^10.1.1",
"style-loader": "^2.0.0",
"stylelint": "^13.12.0",
"stylelint-config-recommended-scss": "^4.2.0",
"stylelint-scss": "^3.19.0",
"stylelint-webpack-plugin": "^2.1.1",
"url-loader": "^4.1.1",
"vue-loader": "^15.9.6",
"vue-template-compiler": "^2.6.12",
"webpack": "^5.28.0",
"webpack-cli": "^4.6.0"
}
}

378
src/AdminSettings.vue Normal file
View File

@ -0,0 +1,378 @@
<!--
- @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl>
-
- @author Roeland Jago Douma <roeland@famdouma.nl>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<div id="registration_settings_form">
<SettingsSection
:title="t('registration', 'Registration settings')">
<p>
<input id="admin_approval"
v-model="adminApproval"
type="checkbox"
name="admin_approval"
class="checkbox"
:disabled="loading"
@change="saveData">
<label for="admin_approval">{{ t('registration', 'Require admin approval') }}</label>
</p>
<em>{{ t('registration', 'Enabling "admin approval" will prevent registrations from mobile and desktop clients to complete as the credentials can not be verified by the client until the user was enabled.') }}</em>
<p>
<label for="registered_user_group">
{{ t('registration', 'Registered users default group') }}
</label>
<Multiselect
id="registered_user_group"
v-model="registeredUserGroup"
:placeholder="t('registration', 'Select group')"
:options="groups"
:disabled="loading"
:searchable="true"
:tag-width="60"
:loading="loadingGroups"
:allow-empty="true"
:close-on-select="false"
track-by="id"
label="displayname"
@search-change="searchGroup"
@change="saveData" />
</p>
</SettingsSection>
<SettingsSection
:title="t('registration', 'Email settings')">
<p>
<label for="allowed_domains">{{ domainListLabel }}</label>
<input
id="allowed_domains"
v-model="allowedDomains"
type="text"
name="allowed_domains"
:disabled="loading"
placeholder="nextcloud.com;*.example.com"
:aria-label="t('registration', 'Allowed email domain')"
@input="debounceSavingSlow">
</p>
<p>
<input id="domains_is_blocklist"
v-model="domainsIsBlocklist"
type="checkbox"
name="domains_is_blocklist"
class="checkbox"
:disabled="loading"
@change="saveData">
<label for="domains_is_blocklist">{{ t('registration', 'Block listed email domains instead of allowing them') }}</label>
</p>
<p>
<input id="show_domains"
v-model="showDomains"
type="checkbox"
name="show_domains"
class="checkbox"
:disabled="loading"
@change="saveData">
<label for="show_domains">{{ showDomainListLabel }}</label>
</p>
<p>
<input id="disable_email_verification"
v-model="disableEmailVerification"
type="checkbox"
name="disable_email_verification"
class="checkbox"
:disabled="loading"
@change="saveData">
<label for="disable_email_verification">{{ t('registration', 'Disable email verification') }}</label>
</p>
</SettingsSection>
<SettingsSection
:title="t('registration', 'User settings')">
<p>
<input id="email_is_login"
v-model="emailIsLogin"
type="checkbox"
name="email_is_login"
class="checkbox"
:disabled="loading"
@change="saveData">
<label for="email_is_login">{{ t('registration', 'Force email as login name') }}</label>
</p>
<template
v-if="!emailIsLogin">
<p>
<label for="username_policy_regex">{{ t('registration', 'Login name policy') }}</label>
<input
id="username_policy_regex"
v-model="usernamePolicyRegex"
type="text"
name="username_policy_regex"
:disabled="loading"
placeholder="E.g.: /^[a-z-]+\.[a-z-]+$/"
:aria-label="t('registration', 'Regular expression to validate login names')"
@input="debounceSavingSlow">
</p>
<em>{{ t('registration', 'If configured, login names will be validated through the regular expression. If the validation fails the user is prompted with a generic error. Make sure your regex is working correctly.') }}</em>
</template>
<p>
<input id="show_fullname"
v-model="showFullname"
type="checkbox"
name="show_fullname"
class="checkbox"
:disabled="loading"
@change="saveData">
<label for="show_fullname">{{ t('registration', 'Show full name field') }}</label>
</p>
<p
v-if="showFullname"
class="indent">
<input id="enforce_fullname"
v-model="enforceFullname"
type="checkbox"
name="enforce_fullname"
class="checkbox"
:disabled="loading"
@change="saveData">
<label for="enforce_fullname">{{ t('registration', 'Enforce full name field') }}</label>
</p>
<p
v-if="canShowPhone">
<input id="show_phone"
v-model="showPhone"
type="checkbox"
name="show_phone"
class="checkbox"
:disabled="loading"
@change="saveData">
<label for="show_phone">{{ t('registration', 'Show phone field') }}</label>
</p>
<p
v-if="canShowPhone && showPhone"
class="indent">
<input id="enforce_phone"
v-model="enforcePhone"
type="checkbox"
name="enforce_phone"
class="checkbox"
:disabled="loading"
@change="saveData">
<label for="enforce_phone">{{ t('registration', 'Enforce phone field') }}</label>
</p>
</SettingsSection>
<SettingsSection
:title="t('registration', 'User instructions')"
:description="t('registration', 'Caution: The user instructions will not be translated and will therefore be displayed as configured below for all users regardless of their actual language.')">
<h3>{{ t('registration', 'Registration form instructions') }}</h3>
<p>
<input v-model="additionalHint"
type="text"
name="additional_hint"
:disabled="loading"
placeholder="Please create your username following the scheme 'firstname.lastname'."
:aria-label="t('registration', 'A short message that is shown to the user in the registration process.')"
@input="debounceSavingSlow">
</p>
<em>{{ t('registration', 'Add additional user instructions (e.g. for choosing their login name). If configured the text is displayed in the account creation step of the registration process.') }}</em>
<h3>{{ t('registration', 'Verification email instructions') }}</h3>
<p>
<input v-model="emailVerificationHint"
type="text"
name="email_verification_hint"
:disabled="loading"
placeholder="Please create your username following the scheme 'firstname.lastname'."
:aria-label="t('registration', 'A short message that is shown to the user in the verification email.')"
@input="debounceSavingSlow">
</p>
<em>{{ t('registration', 'Add additional user instructions (e.g. for choosing their login name). If configured the text is embedded in the verification-email.') }}</em>
</SettingsSection>
</div>
</template>
<script>
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection'
import axios from '@nextcloud/axios'
import { showError, showSuccess } from '@nextcloud/dialogs'
import '@nextcloud/dialogs/styles/toast.scss'
import { loadState } from '@nextcloud/initial-state'
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
import debounce from 'debounce'
export default {
name: 'AdminSettings',
components: {
Multiselect,
SettingsSection,
},
data() {
return {
loading: false,
loadingGroups: false,
groups: [],
saveNotification: null,
adminApproval: false,
registeredUserGroup: '',
allowedDomains: '',
domainsIsBlocklist: false,
showDomains: false,
disableEmailVerification: false,
emailIsLogin: false,
usernamePolicyRegex: '',
showFullname: false,
enforceFullname: false,
canShowPhone: false,
showPhone: false,
enforcePhone: false,
additionalHint: '',
emailVerificationHint: '',
}
},
computed: {
domainListLabel() {
if (this.domainsIsBlocklist) {
return t('registration', 'Blocked email domains')
}
return t('registration', 'Allowed email domains')
},
showDomainListLabel() {
if (this.domainsIsBlocklist) {
return t('registration', 'Show the blocked email domains to users')
}
return t('registration', 'Show the allowed email domains to users')
},
},
mounted() {
this.adminApproval = loadState('registration', 'admin_approval_required')
this.registeredUserGroup = loadState('registration', 'registered_user_group')
this.allowedDomains = loadState('registration', 'allowed_domains')
this.domainsIsBlocklist = loadState('registration', 'domains_is_blocklist')
this.showDomains = loadState('registration', 'show_domains')
this.disableEmailVerification = loadState('registration', 'disable_email_verification')
this.emailIsLogin = loadState('registration', 'email_is_login')
this.usernamePolicyRegex = loadState('registration', 'username_policy_regex')
this.showFullname = loadState('registration', 'show_fullname')
this.enforceFullname = loadState('registration', 'enforce_fullname')
this.canShowPhone = loadState('registration', 'can_show_phone')
this.showPhone = loadState('registration', 'show_phone')
this.enforcePhone = loadState('registration', 'enforce_phone')
this.additionalHint = loadState('registration', 'additional_hint')
this.emailVerificationHint = loadState('registration', 'email_verification_hint')
this.searchGroup('')
},
methods: {
debounceSavingSlow: debounce(function() {
this.saveData()
}, 2000),
async saveData() {
this.loading = true
if (this.saveNotification) {
await this.saveNotification.hideToast()
}
try {
const response = await axios.post(generateUrl('/apps/registration/settings'), {
admin_approval_required: this.adminApproval,
registered_user_group: this.registeredUserGroup?.id,
allowed_domains: this.allowedDomains,
domains_is_blocklist: this.domainsIsBlocklist,
show_domains: this.showDomains,
disable_email_verification: this.disableEmailVerification,
email_is_login: this.emailIsLogin,
username_policy_regex: this.usernamePolicyRegex,
show_fullname: this.showFullname,
enforce_fullname: this.enforceFullname,
show_phone: this.showPhone,
enforce_phone: this.enforcePhone,
additional_hint: this.additionalHint,
email_verification_hint: this.emailVerificationHint,
})
if (response?.data?.status === 'success' && response?.data?.data?.message) {
this.saveNotification = showSuccess(response.data.data.message)
} else if (response?.data?.data?.message) {
this.saveNotification = showError(response.data.data.message)
} else {
this.saveNotification = showError(t('registration', 'An error occurred while saving the settings'))
}
} catch (e) {
if (e.response?.data?.data?.message) {
this.saveNotification = showError(e.response.data.data.message)
} else {
this.saveNotification = showError(t('registration', 'An error occurred while saving the settings'))
console.error(e)
}
}
this.loading = false
},
searchGroup: debounce(async function(query) {
this.loadingGroups = true
try {
const response = await axios.get(generateOcsUrl('cloud', 2) + 'groups/details', {
search: query,
limit: 20,
offset: 0,
})
this.groups = response.data.ocs.data.groups.sort(function(a, b) {
return a.displayname.localeCompare(b.displayname)
})
} catch (err) {
console.error('Could not fetch groups', err)
} finally {
this.loadingGroups = false
}
}, 500),
},
}
</script>
<style scoped lang="scss">
p {
label {
display: block;
}
&.indent {
padding-left: 28px;
}
}
</style>

30
src/settings.js Normal file
View File

@ -0,0 +1,30 @@
/**
* @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import Vue from 'vue'
import AdminSettings from './AdminSettings.vue'
Vue.prototype.t = t
Vue.prototype.OC = OC
export const app = new Vue({
el: '#registration_settings_form',
render: h => h(AdminSettings),
})

32
stylelint.config.js Normal file
View File

@ -0,0 +1,32 @@
module.exports = {
extends: 'stylelint-config-recommended-scss',
rules: {
indentation: 'tab',
'selector-type-no-unknown': null,
'number-leading-zero': null,
'rule-empty-line-before': [
'always',
{
ignore: ['after-comment', 'inside-block'],
},
],
'declaration-empty-line-before': [
'never',
{
ignore: ['after-declaration'],
},
],
'comment-empty-line-before': null,
'selector-type-case': null,
'selector-list-comma-newline-after': null,
'no-descending-specificity': null,
'string-quotes': 'single',
'selector-pseudo-element-no-unknown': [
true,
{
ignorePseudoElements: ['v-deep'],
},
],
},
plugins: ['stylelint-scss'],
}

View File

@ -1,95 +1,7 @@
<?php <?php
/** @var array $_ */ /** @var array $_ */
/** @var \OCP\IL10N $l */ /** @var \OCP\IL10N $l */
script('registration', 'settings'); script('registration', 'registration-settings');
style('registration', 'settings'); style('registration', 'settings');
?> ?>
<form id="registration_settings_form" class="section"> <div id="registration_settings_form"></div>
<h2><?php p($l->t('Registration')); ?></h2><span id="registration_settings_msg" class="msg"></span>
<h3><?php p($l->t('Registered users default group')); ?></h3>
<p>
<label>
<select id="registered_user_group" name="registered_user_group">
<option value="none" <?php echo $_['current'] === 'none' ? 'selected="selected"' : ''; ?>><?php p($l->t('None')); ?></option>
<?php
foreach ($_['groups'] as $group) {
$selected = $_['current'] === $group ? 'selected="selected"' : '';
echo '<option value="'.$group.'" '.$selected.'>'.$group.'</option>';
}
?>
</select>
</label>
</p>
<h3><?php p($l->t('Disable Email Verification')); ?></h3>
<p>
<input type="checkbox" id="disable_email_verification" class="checkbox" name="disable_email_verification" <?php if ($_['disable_email_verification'] === 'yes') {
echo ' checked';
} ?>>
<label for="disable_email_verification"><?php p($l->t('Let user can register directly without email verification')); ?></label>
</p>
<h3><?php p($l->t('Allowed email domains')); ?></h3>
<p>
<label>
<input type="text" id="allowed_domains" name="allowed_domains" value="<?php p($_['allowed']);?>" placeholder="nextcloud.com;*.example.com">
</label>
</p>
<em><?php p($l->t('Enter a semicolon-separated list of allowed email domains, * for wildcard. Example: %s', ['nextcloud.com;*.example.com']));?></em>
<p>
<input type="checkbox" id="domains_is_blocklist" class="checkbox" name="domains_is_blocklist" <?php if ($_['domains_is_blocklist'] === 'yes') {
echo ' checked';
} ?>>
<label for="domains_is_blocklist"><?php p($l->t('Block listed email domains instead of allowing them')); ?></label>
</p>
<p>
<input type="checkbox" id="show_domains" class="checkbox" name="show_domains" <?php if ($_['show_domains'] === 'yes') {
echo ' checked';
} ?>>
<label for="show_domains"><?php p($l->t('Show the allowed/blocked email domains to users')); ?></label>
</p>
<p>
<input type="checkbox" id="email_is_login" class="checkbox" name="email_is_login" <?php if ($_['email_is_login'] === 'yes') {
echo ' checked';
} ?>>
<label for="email_is_login"><?php p($l->t('Force email as login name')); ?></label>
</p>
<h3><?php p($l->t('Username policy')); ?></h3>
<p>
<label>
<input type="text" id="username_policy_regex" name="username_policy_regex" value="<?php p($_['username_policy_regex']);?>" placeholder="E.g.: /^[a-z-]+\.[a-z-]+$/">
</label>
</p>
<em><?php p($l->t('If configured, usernames will be validated through the regular expression. If the validation fails the user is prompted with a generic error. Make sure your regex is working correctly.'));?></em>
<h3><?php p($l->t('User instructions')); ?></h3>
<em><?php p($l->t('Caution: The user instructions will not be translated and will therefore be displayed as configured below for all users regardless of their actual language.'));?></em>
<p>
<label>
<input type="text" id="additional_hint" name="additional_hint" value="<?php p($_['additional_hint']);?>" placeholder="Please create your username following the scheme 'firstname.lastname'.">
</label>
</p>
<em><?php p($l->t('Add additional user instructions (e.g. for choosing their usernames). If configured the text is displayed in the account creation step of the registration process.'));?></em>
<p>
<label>
<input type="text" id="email_verification_hint" name="email_verification_hint" value="<?php p($_['email_verification_hint']);?>" placeholder="Please create your username following the scheme 'firstname.lastname'.">
</label>
</p>
<em><?php p($l->t('Add additional user instructions (e.g. for choosing their usernames). If configured the text is embedded in the the verification-Email.'));?></em>
<h3><?php p($l->t('Admin approval')); ?></h3>
<p>
<input type="checkbox" id="admin_approval_required" class="checkbox" name="admin_approval_required" <?php if ($_['approval_required'] === 'yes') {
echo ' checked';
} ?>>
<label for="admin_approval_required"><?php p($l->t('Require admin approval')); ?></label>
</p>
<em><?php p($l->t('Enabling "admin approval" will prevent registrations from mobile and desktop clients to complete as the credentials can not be verified by the client until the user was enabled.'));?></em>
</form>

View File

@ -20,6 +20,8 @@ style('registration', 'style');
<input type="submit" id="submit" value="<?php <input type="submit" id="submit" value="<?php
if ($_['disable_email_verification'] === 'yes') { if ($_['disable_email_verification'] === 'yes') {
p($l->t('Continue')); p($l->t('Continue'));
} elseif ($_['is_login_flow']) {
p($l->t('Request verification code'));
} else { } else {
p($l->t('Request verification link')); p($l->t('Request verification link'));
} ?>" /> } ?>" />

View File

@ -31,18 +31,44 @@ script('registration', 'form');
<?php if (!$_['email_is_login']) { ?> <?php if (!$_['email_is_login']) { ?>
<p class="groupmiddle"> <p class="groupmiddle">
<input type="text" name="username" id="username" value="<?php if (!empty($_['entered_data']['user'])) { <input type="text" name=loginname" id="loginname" value="<?php if (!empty($_['login'])) {
p($_['entered_data']['user']); p($_['login']);
} ?>" placeholder="<?php p($l->t('Username')); ?>" /> } ?>" placeholder="<?php p($l->t('Login name')); ?>" />
<label for="username" class="infield"><?php p($l->t('Username')); ?></label> <label for="loginname" class="infield"><?php p($l->t('Login name')); ?></label>
<img id="username-icon" class="svg" src="<?php print_unescaped(image_path('', 'actions/user.svg')); ?>" alt=""/> <img id="loginname-icon" class="svg" src="<?php print_unescaped(image_path('', 'categories/auth.svg')); ?>" alt=""/>
</p> </p>
<?php } else { ?> <?php } else { ?>
<input type="hidden" name="username" value="<?php p($_['email']); ?>" /> <input type="hidden" name="loginname" value="<?php p($_['email']); ?>" />
<?php } ?>
<?php if ($_['show_fullname']) { ?>
<p class="groupmiddle">
<input type="text" name="fullname" id="fullname" value="<?php if (!empty($_['fullname'])) {
p($_['fullname']);
} ?>" placeholder="<?php p($l->t('Full name')); ?>" />
<label for="fullname" class="infield"><?php p($l->t('Full name')); ?></label>
<img id="fullname-icon" class="svg" src="<?php print_unescaped(image_path('', 'actions/user.svg')); ?>" alt=""/>
</p>
<?php } else { ?>
<input type="hidden" name="fullname" value="" />
<?php } ?>
<?php if ($_['show_phone']) { ?>
<p class="groupmiddle">
<input type="text" name="phone" id="phone" value="<?php if (!empty($_['phone'])) {
p($_['phone']);
} ?>" placeholder="<?php p($l->t('Phone number')); ?>" />
<label for="phone" class="infield"><?php p($l->t('Phone number')); ?></label>
<img id="phone-icon" class="svg" src="<?php print_unescaped(image_path('', 'clients/phone.svg')); ?>" alt=""/>
</p>
<?php } else { ?>
<input type="hidden" name="phone" value="" />
<?php } ?> <?php } ?>
<p class="groupbottom"> <p class="groupbottom">
<input type="password" name="password" id="password" placeholder="<?php p($l->t('Password')); ?>"/> <input type="password" name="password" id="password" value="<?php if (!empty($_['password'])) {
p($_['password']);
} ?>" placeholder="<?php p($l->t('Password')); ?>"/>
<label for="password" class="infield"><?php p($l->t('Password')); ?></label> <label for="password" class="infield"><?php p($l->t('Password')); ?></label>
<img id="password-icon" class="svg" src="<?php print_unescaped(image_path('', 'actions/password.svg')); ?>" alt=""/> <img id="password-icon" class="svg" src="<?php print_unescaped(image_path('', 'actions/password.svg')); ?>" alt=""/>
<a id="showadminpass" href="#" class="toggle-password"> <a id="showadminpass" href="#" class="toggle-password">

View File

@ -127,6 +127,9 @@ class RegisterControllerTest extends TestCase {
$response = $controller->showEmailForm($email, $message); $response = $controller->showEmailForm($email, $message);
$disable_email_verification = $this->config->getAppValue('registration', 'disable_email_verification', 'no'); $disable_email_verification = $this->config->getAppValue('registration', 'disable_email_verification', 'no');
$this->loginFlowService->method('isUsingLoginFlow')
->willReturn(false);
self::assertSame(TemplateResponse::RENDER_AS_GUEST, $response->getRenderAs()); self::assertSame(TemplateResponse::RENDER_AS_GUEST, $response->getRenderAs());
self::assertSame('form/email', $response->getTemplateName()); self::assertSame('form/email', $response->getTemplateName());
@ -134,6 +137,7 @@ class RegisterControllerTest extends TestCase {
'email' => $email, 'email' => $email,
'message' => $message, 'message' => $message,
'disable_email_verification' => $disable_email_verification, 'disable_email_verification' => $disable_email_verification,
'is_login_flow' => false,
], $response->getParams()); ], $response->getParams());
} }
@ -434,6 +438,9 @@ class RegisterControllerTest extends TestCase {
$secret = '123456789'; $secret = '123456789';
$token = 'abcdefghi'; $token = 'abcdefghi';
$email = 'nextcloud@example.tld'; $email = 'nextcloud@example.tld';
$fullname = 'Full name';
$phone = '0123 / 456789';
$password = '123456';
$registration = Registration::fromParams([ $registration = Registration::fromParams([
'email' => 'nextcloud@example.tld', 'email' => 'nextcloud@example.tld',
@ -443,11 +450,17 @@ class RegisterControllerTest extends TestCase {
'validateSecretAndToken' 'validateSecretAndToken'
]); ]);
$this->config->method('getAppValue')
->willReturnMap([
['registration', 'show_fullname', 'no', 'yes'],
['registration', 'show_phone', 'no', 'yes'],
]);
$controller->expects($this->once()) $controller->expects($this->once())
->method('validateSecretAndToken') ->method('validateSecretAndToken')
->willReturn($registration); ->willReturn($registration);
$response = $controller->showUserForm($secret, $token, $username, $message); $response = $controller->showUserForm($secret, $token, $username, $fullname, $phone, $password, $message);
self::assertSame(TemplateResponse::RENDER_AS_GUEST, $response->getRenderAs()); self::assertSame(TemplateResponse::RENDER_AS_GUEST, $response->getRenderAs());
self::assertSame('form/user', $response->getTemplateName()); self::assertSame('form/user', $response->getTemplateName());
@ -455,8 +468,13 @@ class RegisterControllerTest extends TestCase {
self::assertSame([ self::assertSame([
'email' => $email, 'email' => $email,
'email_is_login' => false, 'email_is_login' => false,
'username' => $username, 'loginname' => $username,
'fullname' => $fullname,
'show_fullname' => true,
'phone' => $phone,
'show_phone' => true,
'message' => $message, 'message' => $message,
'password' => $password,
'additional_hint' => null, 'additional_hint' => null,
], $response->getParams()); ], $response->getParams());
} }
@ -500,7 +518,7 @@ class RegisterControllerTest extends TestCase {
->method('validateSecretAndTokenErrorPage') ->method('validateSecretAndTokenErrorPage')
->willReturn($response); ->willReturn($response);
self::assertSame($response, $controller->submitUserForm($secret, $token, '', '')); self::assertSame($response, $controller->submitUserForm($secret, $token, '', '', '', ''));
} }
public function testSubmitUserFormCreateAccountException(): void { public function testSubmitUserFormCreateAccountException(): void {
@ -508,6 +526,8 @@ class RegisterControllerTest extends TestCase {
$token = 'abcdefghi'; $token = 'abcdefghi';
$username = 'user'; $username = 'user';
$password = 'password'; $password = 'password';
$fullname = 'Full name';
$phone = '0123 / 456789';
$registration = Registration::fromParams([ $registration = Registration::fromParams([
'email' => 'nextcloud@example.tld', 'email' => 'nextcloud@example.tld',
@ -529,10 +549,10 @@ class RegisterControllerTest extends TestCase {
$this->registrationService->expects($this->once()) $this->registrationService->expects($this->once())
->method('createAccount') ->method('createAccount')
->with($registration, $username, $password) ->with($registration, $username, $fullname, $phone, $password)
->willThrowException(new RegistrationException('Invalid account data')); ->willThrowException(new RegistrationException('Invalid account data'));
self::assertSame($response, $controller->submitUserForm($secret, $token, $username, $password)); self::assertSame($response, $controller->submitUserForm($secret, $token, $username, $fullname, $phone, $password));
} }
public function testSubmitUserFormRequiresAdminApproval(): void { public function testSubmitUserFormRequiresAdminApproval(): void {
@ -540,6 +560,8 @@ class RegisterControllerTest extends TestCase {
$token = 'abcdefghi'; $token = 'abcdefghi';
$username = 'user'; $username = 'user';
$password = 'password'; $password = 'password';
$fullname = 'Full name';
$phone = '0123 / 456789';
$registration = Registration::fromParams([ $registration = Registration::fromParams([
'email' => 'nextcloud@example.tld', 'email' => 'nextcloud@example.tld',
@ -560,14 +582,14 @@ class RegisterControllerTest extends TestCase {
$this->registrationService->expects($this->once()) $this->registrationService->expects($this->once())
->method('createAccount') ->method('createAccount')
->with($registration, $username, $password) ->with($registration, $username, $fullname, $phone, $password)
->willReturn($user); ->willReturn($user);
$this->registrationService->expects($this->once()) $this->registrationService->expects($this->once())
->method('deleteRegistration') ->method('deleteRegistration')
->with($registration); ->with($registration);
$response = $controller->submitUserForm($secret, $token, $username, $password); $response = $controller->submitUserForm($secret, $token, $username, $fullname, $phone, $password);
self::assertInstanceOf(StandaloneTemplateResponse::class, $response); self::assertInstanceOf(StandaloneTemplateResponse::class, $response);
self::assertSame(TemplateResponse::RENDER_AS_GUEST, $response->getRenderAs()); self::assertSame(TemplateResponse::RENDER_AS_GUEST, $response->getRenderAs());
@ -579,6 +601,8 @@ class RegisterControllerTest extends TestCase {
$token = 'abcdefghi'; $token = 'abcdefghi';
$username = 'user'; $username = 'user';
$password = 'password'; $password = 'password';
$fullname = 'Full name';
$phone = '0123 / 456789';
$registration = Registration::fromParams([ $registration = Registration::fromParams([
'email' => 'nextcloud@example.tld', 'email' => 'nextcloud@example.tld',
@ -601,7 +625,7 @@ class RegisterControllerTest extends TestCase {
$this->registrationService->expects($this->once()) $this->registrationService->expects($this->once())
->method('createAccount') ->method('createAccount')
->with($registration, $username, $password) ->with($registration, $username, $fullname, $phone, $password)
->willReturn($user); ->willReturn($user);
$this->registrationService->expects($this->once()) $this->registrationService->expects($this->once())
@ -612,7 +636,7 @@ class RegisterControllerTest extends TestCase {
->method('loginUser') ->method('loginUser')
->with($username, $username, $password); ->with($username, $username, $password);
$response = $controller->submitUserForm($secret, $token, $username, $password); $response = $controller->submitUserForm($secret, $token, $username, $fullname, $phone, $password);
self::assertInstanceOf(RedirectToDefaultAppResponse::class, $response); self::assertInstanceOf(RedirectToDefaultAppResponse::class, $response);
} }
@ -622,6 +646,8 @@ class RegisterControllerTest extends TestCase {
$token = 'abcdefghi'; $token = 'abcdefghi';
$username = 'user'; $username = 'user';
$password = 'password'; $password = 'password';
$fullname = 'Full name';
$phone = '0123 / 456789';
$registration = Registration::fromParams([ $registration = Registration::fromParams([
'email' => 'nextcloud@example.tld', 'email' => 'nextcloud@example.tld',
@ -644,7 +670,7 @@ class RegisterControllerTest extends TestCase {
$this->registrationService->expects($this->once()) $this->registrationService->expects($this->once())
->method('createAccount') ->method('createAccount')
->with($registration, $username, $password) ->with($registration, $username, $fullname, $phone, $password)
->willReturn($user); ->willReturn($user);
$this->registrationService->expects($this->once()) $this->registrationService->expects($this->once())
@ -664,7 +690,7 @@ class RegisterControllerTest extends TestCase {
->with($user) ->with($user)
->willReturn($response); ->willReturn($response);
self::assertSame($response, $controller->submitUserForm($secret, $token, $username, $password)); self::assertSame($response, $controller->submitUserForm($secret, $token, $username, $fullname, $phone, $password));
} }
public function testSubmitUserFormSuccessfulLoginFlow1(): void { public function testSubmitUserFormSuccessfulLoginFlow1(): void {
@ -672,6 +698,8 @@ class RegisterControllerTest extends TestCase {
$token = 'abcdefghi'; $token = 'abcdefghi';
$username = 'user'; $username = 'user';
$password = 'password'; $password = 'password';
$fullname = 'Full name';
$phone = '0123 / 456789';
$registration = Registration::fromParams([ $registration = Registration::fromParams([
'email' => 'nextcloud@example.tld', 'email' => 'nextcloud@example.tld',
@ -694,7 +722,7 @@ class RegisterControllerTest extends TestCase {
$this->registrationService->expects($this->once()) $this->registrationService->expects($this->once())
->method('createAccount') ->method('createAccount')
->with($registration, $username, $password) ->with($registration, $username, $fullname, $phone, $password)
->willReturn($user); ->willReturn($user);
$this->registrationService->expects($this->once()) $this->registrationService->expects($this->once())
@ -715,6 +743,6 @@ class RegisterControllerTest extends TestCase {
$this->loginFlowService->method('tryLoginFlowV1') $this->loginFlowService->method('tryLoginFlowV1')
->willReturn($response); ->willReturn($response);
self::assertSame($response, $controller->submitUserForm($secret, $token, $username, $password)); self::assertSame($response, $controller->submitUserForm($secret, $token, $username, $fullname, $phone, $password));
} }
} }

View File

@ -7,6 +7,7 @@ use OCA\Registration\Db\RegistrationMapper;
use OCA\Registration\Service\MailService; use OCA\Registration\Service\MailService;
use OCA\Registration\Service\RegistrationException; use OCA\Registration\Service\RegistrationException;
use OCA\Registration\Service\RegistrationService; use OCA\Registration\Service\RegistrationService;
use OCP\Accounts\IAccountManager;
use OCP\IConfig; use OCP\IConfig;
use OCP\IGroupManager; use OCP\IGroupManager;
use OCP\IL10N; use OCP\IL10N;
@ -42,6 +43,8 @@ class RegistrationServiceTest extends TestCase {
private $registrationMapper; private $registrationMapper;
/** @var IUserManager */ /** @var IUserManager */
private $userManager; private $userManager;
/** @var IAccountManager */
private $accountManager;
/** @var IConfig */ /** @var IConfig */
private $config; private $config;
/** @var IGroupManager */ /** @var IGroupManager */
@ -75,6 +78,7 @@ class RegistrationServiceTest extends TestCase {
$this->urlGenerator = $this->createMock(IURLGenerator::class); $this->urlGenerator = $this->createMock(IURLGenerator::class);
#$this->userManager = $this->createMock(IUserManager::class); #$this->userManager = $this->createMock(IUserManager::class);
$this->userManager = \OC::$server->getUserManager(); $this->userManager = \OC::$server->getUserManager();
$this->accountManager = $this->createMock(IAccountManager::class);
$this->config = $this->createMock(IConfig::class); $this->config = $this->createMock(IConfig::class);
$this->groupManager = \OC::$server->getGroupManager(); $this->groupManager = \OC::$server->getGroupManager();
$this->random = \OC::$server->getSecureRandom(); $this->random = \OC::$server->getSecureRandom();
@ -97,6 +101,7 @@ class RegistrationServiceTest extends TestCase {
$this->urlGenerator, $this->urlGenerator,
$this->registrationMapper, $this->registrationMapper,
$this->userManager, $this->userManager,
$this->accountManager,
$this->config, $this->config,
$this->groupManager, $this->groupManager,
$this->random, $this->random,
@ -219,7 +224,7 @@ class RegistrationServiceTest extends TestCase {
$form_input_username = 'alice1'; $form_input_username = 'alice1';
$resulting_user = $this->service->createAccount($reg, $form_input_username, 'asdf'); $resulting_user = $this->service->createAccount($reg, $form_input_username, 'Full name', '+49 800 / 1110111', 'asdf');
$this->assertInstanceOf(IUser::class, $resulting_user); $this->assertInstanceOf(IUser::class, $resulting_user);
$this->assertEquals($form_input_username, $resulting_user->getUID()); $this->assertEquals($form_input_username, $resulting_user->getUID());
@ -238,8 +243,8 @@ class RegistrationServiceTest extends TestCase {
$reg->setEmailConfirmed(true); $reg->setEmailConfirmed(true);
$this->expectException(RegistrationException::class); $this->expectException(RegistrationException::class);
$this->expectExceptionMessage('The username you have chosen already exists.'); $this->expectExceptionMessage('The login name you have chosen already exists.');
$this->service->createAccount($reg, 'alice1', 'asdf'); $this->service->createAccount($reg, 'alice1', 'Full name', '+49 800 / 1110111', 'asdf');
} }
/* /*
@ -258,12 +263,16 @@ class RegistrationServiceTest extends TestCase {
$reg->setEmail("pppp@example.com"); $reg->setEmail("pppp@example.com");
$reg->setUsername("alice1"); $reg->setUsername("alice1");
$reg->setDisplayname("Alice"); $reg->setDisplayname("Alice");
$reg->setPassword("asdf"); $reg->setPassword("crypto(asdf)");
$reg->setEmailConfirmed(true); $reg->setEmailConfirmed(true);
$this->crypto->method('decrypt')
->with('crypto(asdf)')
->willReturn('asdf');
$this->expectException(RegistrationException::class); $this->expectException(RegistrationException::class);
$this->expectExceptionMessage('The username you have chosen already exists.'); $this->expectExceptionMessage('The login name you have chosen already exists.');
$this->service->createAccount($reg); $this->service->createAccount($reg, null, 'Full name', '+49 800 / 1110111');
} }
/** /**
@ -280,12 +289,16 @@ class RegistrationServiceTest extends TestCase {
$reg->setEmail("pppp@example.com"); $reg->setEmail("pppp@example.com");
$reg->setUsername("alice23"); $reg->setUsername("alice23");
$reg->setDisplayname("Alice"); $reg->setDisplayname("Alice");
$reg->setPassword("asdf"); $reg->setPassword("crypto(asdf)");
$reg->setEmailConfirmed(true); $reg->setEmailConfirmed(true);
$this->crypto->method('decrypt')
->with('crypto(asdf)')
->willReturn('asdf');
$this->expectException(RegistrationException::class); $this->expectException(RegistrationException::class);
$this->expectExceptionMessage('Please provide a valid user name.'); $this->expectExceptionMessage('Please provide a valid login name.');
$this->service->createAccount($reg); $this->service->createAccount($reg, null, 'Full name', '+49 800 / 1110111');
} }
public function settingsCallback1($app, $key, $default) { public function settingsCallback1($app, $key, $default) {
@ -293,6 +306,10 @@ class RegistrationServiceTest extends TestCase {
'registered_user_group' => 'none', 'registered_user_group' => 'none',
'admin_approval_required' => 'no', 'admin_approval_required' => 'no',
'username_policy_regex' => '', 'username_policy_regex' => '',
'show_fullname' => 'yes',
'enforce_fullname' => 'no',
'show_phone' => 'yes',
'enforce_phone' => 'no',
]; ];
return $map[$key]; return $map[$key];

8
webpack.js Normal file
View File

@ -0,0 +1,8 @@
const path = require('path')
const webpackConfig = require('@nextcloud/webpack-vue-config')
webpackConfig.entry = {
settings: path.join(__dirname, 'src', 'settings'),
}
module.exports = webpackConfig