mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-20 17:15:51 +02:00
Mark SAML users as so in database and disable local password changing then.
This commit is contained in:
parent
78ec0f1ea3
commit
97c3b9002a
15 changed files with 1414 additions and 1264 deletions
40
migrations/Version20230220221024.php
Normal file
40
migrations/Version20230220221024.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Migration\AbstractMultiPlatformMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20230220221024 extends AbstractMultiPlatformMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Added support for SAML/Keycloak';
|
||||
}
|
||||
|
||||
public function mySQLUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE `users` ADD saml_user TINYINT(1) NOT NULL DEFAULT 0');
|
||||
}
|
||||
|
||||
public function mySQLDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE `users` DROP saml_user');
|
||||
}
|
||||
|
||||
public function sqLiteUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE users ADD saml_user BOOLEAN NOT NULL DEFAULT 0');
|
||||
}
|
||||
|
||||
public function sqLiteDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE `users` DROP saml_user');
|
||||
}
|
||||
}
|
|
@ -83,6 +83,10 @@ class UserSettingsController extends AbstractController
|
|||
return new RuntimeException('This controller only works only for Part-DB User objects!');
|
||||
}
|
||||
|
||||
if ($user->isSamlUser()) {
|
||||
throw new RuntimeException('You can not remove U2F keys from SAML users!');
|
||||
}
|
||||
|
||||
if (empty($user->getBackupCodes())) {
|
||||
$this->addFlash('error', 'tfa_backup.no_codes_enabled');
|
||||
|
||||
|
@ -112,6 +116,10 @@ class UserSettingsController extends AbstractController
|
|||
throw new RuntimeException('This controller only works only for Part-DB User objects!');
|
||||
}
|
||||
|
||||
if ($user->isSamlUser()) {
|
||||
throw new RuntimeException('You can not remove U2F keys from SAML users!');
|
||||
}
|
||||
|
||||
if ($this->isCsrfTokenValid('delete'.$user->getId(), $request->request->get('_token'))) {
|
||||
//Handle U2F key removal
|
||||
if ($request->request->has('key_id')) {
|
||||
|
@ -192,6 +200,10 @@ class UserSettingsController extends AbstractController
|
|||
return new RuntimeException('This controller only works only for Part-DB User objects!');
|
||||
}
|
||||
|
||||
if ($user->isSamlUser()) {
|
||||
throw new RuntimeException('You can not remove U2F keys from SAML users!');
|
||||
}
|
||||
|
||||
if ($this->isCsrfTokenValid('devices_reset'.$user->getId(), $request->request->get('_token'))) {
|
||||
$user->invalidateTrustedDeviceTokens();
|
||||
$entityManager->flush();
|
||||
|
@ -281,14 +293,14 @@ class UserSettingsController extends AbstractController
|
|||
])
|
||||
->add('old_password', PasswordType::class, [
|
||||
'label' => 'user.settings.pw_old.label',
|
||||
'disabled' => $this->demo_mode,
|
||||
'disabled' => $this->demo_mode || $user->isSamlUser(),
|
||||
'attr' => [
|
||||
'autocomplete' => 'current-password',
|
||||
],
|
||||
'constraints' => [new UserPassword()],
|
||||
]) //This constraint checks, if the current user pw was inputted.
|
||||
->add('new_password', RepeatedType::class, [
|
||||
'disabled' => $this->demo_mode,
|
||||
'disabled' => $this->demo_mode || $user->isSamlUser(),
|
||||
'type' => PasswordType::class,
|
||||
'first_options' => [
|
||||
'label' => 'user.settings.pw_new.label',
|
||||
|
@ -307,7 +319,10 @@ class UserSettingsController extends AbstractController
|
|||
'max' => 128,
|
||||
])],
|
||||
])
|
||||
->add('submit', SubmitType::class, ['label' => 'save'])
|
||||
->add('submit', SubmitType::class, [
|
||||
'label' => 'save',
|
||||
'disabled' => $this->demo_mode || $user->isSamlUser(),
|
||||
])
|
||||
->getForm();
|
||||
|
||||
$pw_form->handleRequest($request);
|
||||
|
@ -327,7 +342,9 @@ class UserSettingsController extends AbstractController
|
|||
}
|
||||
|
||||
//Handle 2FA things
|
||||
$google_form = $this->createForm(TFAGoogleSettingsType::class, $user);
|
||||
$google_form = $this->createForm(TFAGoogleSettingsType::class, $user, [
|
||||
'disabled' => $this->demo_mode || $user->isSamlUser(),
|
||||
]);
|
||||
$google_enabled = $user->isGoogleAuthenticatorEnabled();
|
||||
if (!$google_enabled && !$form->isSubmitted()) {
|
||||
$user->setGoogleAuthenticatorSecret($googleAuthenticator->generateSecret());
|
||||
|
@ -335,7 +352,7 @@ class UserSettingsController extends AbstractController
|
|||
}
|
||||
$google_form->handleRequest($request);
|
||||
|
||||
if (!$this->demo_mode && $google_form->isSubmitted() && $google_form->isValid()) {
|
||||
if (!$this->demo_mode && !$user->isSamlUser() && $google_form->isSubmitted() && $google_form->isValid()) {
|
||||
if (!$google_enabled) {
|
||||
//Save 2FA settings (save secrets)
|
||||
$user->setGoogleAuthenticatorSecret($google_form->get('googleAuthenticatorSecret')->getData());
|
||||
|
@ -369,7 +386,7 @@ class UserSettingsController extends AbstractController
|
|||
])->getForm();
|
||||
|
||||
$backup_form->handleRequest($request);
|
||||
if (!$this->demo_mode && $backup_form->isSubmitted() && $backup_form->isValid()) {
|
||||
if (!$this->demo_mode && !$user->isSamlUser() && $backup_form->isSubmitted() && $backup_form->isValid()) {
|
||||
$backupCodeManager->regenerateBackupCodes($user);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'user.settings.2fa.backup_codes.regenerated');
|
||||
|
|
|
@ -20,9 +20,11 @@
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Entity\UserSystem\WebauthnKey;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Jbtronics\TFAWebauthn\Services\TFAWebauthnRegistrationHelper;
|
||||
use RuntimeException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
@ -31,6 +33,13 @@ use function Symfony\Component\Translation\t;
|
|||
|
||||
class WebauthnKeyRegistrationController extends AbstractController
|
||||
{
|
||||
private bool $demo_mode;
|
||||
|
||||
public function __construct(bool $demo_mode)
|
||||
{
|
||||
$this->demo_mode = $demo_mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/webauthn/register", name="webauthn_register")
|
||||
*/
|
||||
|
@ -39,6 +48,20 @@ class WebauthnKeyRegistrationController extends AbstractController
|
|||
//When user change its settings, he should be logged in fully.
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
|
||||
if ($this->demo_mode) {
|
||||
throw new RuntimeException('You can not do 2FA things in demo mode');
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new RuntimeException('This controller only works only for Part-DB User objects!');
|
||||
}
|
||||
|
||||
if ($user->isSamlUser()) {
|
||||
throw new RuntimeException('You can not remove U2F keys from SAML users!');
|
||||
}
|
||||
|
||||
//If form was submitted, check the auth response
|
||||
if ($request->getMethod() === 'POST') {
|
||||
$webauthnResponse = $request->request->get('_auth_code');
|
||||
|
|
|
@ -240,10 +240,16 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
|||
|
||||
/**
|
||||
* @var DateTime the time until the password reset token is valid
|
||||
* @ORM\Column(type="datetime", nullable=true)
|
||||
* @ORM\Column(type="datetime", nullable=true, columnDefinition="DEFAULT NULL")
|
||||
*/
|
||||
protected $pw_reset_expires;
|
||||
|
||||
/**
|
||||
* @var bool True if the user was created by a SAML provider (and therefore cannot change its password)
|
||||
* @ORM\Column(type="boolean")
|
||||
*/
|
||||
protected bool $saml_user = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
@ -863,6 +869,28 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
|||
$this->webauthn_keys->add($webauthnKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true, if the user was created by the SAML authentication.
|
||||
* @return bool
|
||||
*/
|
||||
public function isSamlUser(): bool
|
||||
{
|
||||
return $this->saml_user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the saml_user flag.
|
||||
* @param bool $saml_user
|
||||
* @return User
|
||||
*/
|
||||
public function setSamlUser(bool $saml_user): User
|
||||
{
|
||||
$this->saml_user = $saml_user;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function setSamlAttributes(array $attributes)
|
||||
{
|
||||
//When mail attribute exists, set it
|
||||
|
|
|
@ -65,7 +65,7 @@ class UserAdminForm extends AbstractType
|
|||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
/** @var AbstractStructuralDBElement $entity */
|
||||
/** @var User $entity */
|
||||
$entity = $options['data'];
|
||||
$is_new = null === $entity->getID();
|
||||
|
||||
|
@ -164,7 +164,7 @@ class UserAdminForm extends AbstractType
|
|||
'invalid_message' => 'password_must_match',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
'disabled' => !$this->security->isGranted('set_password', $entity),
|
||||
'disabled' => !$this->security->isGranted('set_password', $entity) || $entity->isSamlUser(),
|
||||
'constraints' => [new Length([
|
||||
'min' => 6,
|
||||
'max' => 128,
|
||||
|
@ -174,7 +174,7 @@ class UserAdminForm extends AbstractType
|
|||
->add('need_pw_change', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'user.edit.needs_pw_change',
|
||||
'disabled' => !$this->security->isGranted('set_password', $entity),
|
||||
'disabled' => !$this->security->isGranted('set_password', $entity) || $entity->isSamlUser(),
|
||||
])
|
||||
|
||||
->add('disabled', CheckboxType::class, [
|
||||
|
|
|
@ -57,7 +57,7 @@ class UserSettingsType extends AbstractType
|
|||
$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'user.username.label',
|
||||
'disabled' => !$this->security->isGranted('edit_username', $options['data']) || $this->demo_mode,
|
||||
'disabled' => !$this->security->isGranted('edit_username', $options['data']) || $this->demo_mode || $options['data']->isSamlUser(),
|
||||
])
|
||||
->add('first_name', TextType::class, [
|
||||
'required' => false,
|
||||
|
|
|
@ -45,10 +45,16 @@ class NamedDBElementRepository extends DBElementRepository
|
|||
$node->setId($entity->getID());
|
||||
$result[] = $node;
|
||||
|
||||
if ($entity instanceof User && $entity->isDisabled()) {
|
||||
//If this is an user, then add a badge when it is disabled
|
||||
$node->setIcon('fa-fw fa-treeview fa-solid fa-user-lock text-muted');
|
||||
if ($entity instanceof User) {
|
||||
if ($entity->isDisabled()) {
|
||||
//If this is an user, then add a badge when it is disabled
|
||||
$node->setIcon('fa-fw fa-treeview fa-solid fa-user-lock text-muted');
|
||||
}
|
||||
if ($entity->isSamlUser()) {
|
||||
$node->setIcon('fa-fw fa-treeview fa-solid fa-house-user text-muted');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
|
|
@ -32,6 +32,8 @@ class SamlUserFactory implements SamlUserFactoryInterface
|
|||
$user->setName($username);
|
||||
$user->setNeedPwChange(false);
|
||||
$user->setPassword('$$SAML$$');
|
||||
//This is a SAML user now!
|
||||
$user->setSamlUser(true);
|
||||
|
||||
$user->setSamlAttributes($attributes);
|
||||
|
||||
|
|
|
@ -33,6 +33,10 @@
|
|||
</div>
|
||||
|
||||
<div class="tab-pane" id="password">
|
||||
<div class="offset-3 mb-3 col-9">
|
||||
<span class="badge badge-warning bg-warning"><i class="fa-solid fa-house-user"></i> {% trans %}user.saml_user{% endtrans %}</span>
|
||||
</div>
|
||||
|
||||
{{ form_row(form.new_password) }}
|
||||
{{ form_row(form.need_pw_change) }}
|
||||
{{ form_row(form.disabled) }}
|
||||
|
|
|
@ -151,7 +151,9 @@
|
|||
<p><b>{% trans %}tfa_u2f.no_keys_registered{% endtrans %}</b></p>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ path('webauthn_register') }}" class="btn btn-success"><i class="fas fa-plus-square fa-fw"></i> {% trans %}tfa_u2f.add_new_key{% endtrans %}</a>
|
||||
{% if not user.samlUser %}
|
||||
<a href="{{ path('webauthn_register') }}" class="btn btn-success" ><i class="fas fa-plus-square fa-fw"></i> {% trans %}tfa_u2f.add_new_key{% endtrans %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="tfa-trustedDevices" role="tabpanel" aria-labelledby="trustedDevices-tab-tab">
|
||||
|
@ -163,7 +165,7 @@
|
|||
<input type="hidden" name="_method" value="DELETE">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('devices_reset' ~ user.id) }}">
|
||||
|
||||
<button class="btn btn-danger" type="submit">{% trans %}tfa_trustedDevices.invalidate.btn{% endtrans %}</button>
|
||||
<button class="btn btn-danger" type="submit" {% if user.samlUser %}disabled{% endif %}>{% trans %}tfa_trustedDevices.invalidate.btn{% endtrans %}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -52,9 +52,16 @@
|
|||
<div class="form-group row">
|
||||
<label class="col-form-label col-md-4">{% trans %}group.label{% endtrans %}</label>
|
||||
<div class="col-md-8">
|
||||
<p class="form-control-plaintext">{{ user.group.fullPath }}</p>
|
||||
<p class="form-control-plaintext">{{ user.group.fullPath ?? '' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if user.samlUser %}
|
||||
<div class="form-group row">
|
||||
<div class="col-md-8 offset-md-4">
|
||||
<span class="badge badge-primary bg-primary"><i class="fa-solid fa-house-user"></i> {% trans %}user.saml_user{% endtrans %}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -74,9 +81,9 @@
|
|||
{% endif %}
|
||||
|
||||
<div class="mt-2">
|
||||
{% if datatable is defined and datatable is not null %}
|
||||
{% import "components/history_log_macros.html.twig" as log %}
|
||||
{{ log.element_history_component(datatable) }}
|
||||
{% endif %}
|
||||
{% if datatable is defined and datatable is not null %}
|
||||
{% import "components/history_log_macros.html.twig" as log %}
|
||||
{{ log.element_history_component(datatable) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -52,6 +52,15 @@
|
|||
{% block content %}
|
||||
{{ parent() }}
|
||||
|
||||
{% if user.samlUser %}
|
||||
<div class="alert alert-warning mt-3" role="alert">
|
||||
<h4 class="alert-heading">{% trans %}user.saml_user{% endtrans %}</h4>
|
||||
<p>
|
||||
{% trans %}user.saml_user.pw_change_hint{% endtrans %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% include "users/_2fa_settings.html.twig" %}
|
||||
|
||||
<div class="card mt-4">
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2,7 +2,7 @@
|
|||
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="en">
|
||||
<file id="security.en">
|
||||
<unit id="aazoCks" name="user.login_error.user_disabled">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>user.login_error.user_disabled</source>
|
||||
<target>Your account is disabled! Contact an administrator if you think this is wrong.</target>
|
||||
</segment>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<note priority="1">Part-DB1\src\Entity\UserSystem\Group.php:0</note>
|
||||
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
|
||||
</notes>
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>part.master_attachment.must_be_picture</source>
|
||||
<target>The preview attachment must be a valid picture!</target>
|
||||
</segment>
|
||||
|
@ -82,7 +82,7 @@
|
|||
<note priority="1">src\Entity\StructuralDBElement.php:0</note>
|
||||
<note priority="1">src\Entity\Supplier.php:0</note>
|
||||
</notes>
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>structural.entity.unique_name</source>
|
||||
<target>An element with this name already exists on this level!</target>
|
||||
</segment>
|
||||
|
@ -102,7 +102,7 @@
|
|||
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
|
||||
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
|
||||
</notes>
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>parameters.validator.min_lesser_typical</source>
|
||||
<target>Value must be lesser or equal the the typical value ({{ compared_value }}).</target>
|
||||
</segment>
|
||||
|
@ -122,7 +122,7 @@
|
|||
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
|
||||
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
|
||||
</notes>
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>parameters.validator.min_lesser_max</source>
|
||||
<target>Value must be lesser than the maximum value ({{ compared_value }}).</target>
|
||||
</segment>
|
||||
|
@ -142,7 +142,7 @@
|
|||
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
|
||||
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
|
||||
</notes>
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>parameters.validator.max_greater_typical</source>
|
||||
<target>Value must be greater or equal than the typical value ({{ compared_value }}).</target>
|
||||
</segment>
|
||||
|
@ -152,7 +152,7 @@
|
|||
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
|
||||
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
|
||||
</notes>
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>validator.user.username_already_used</source>
|
||||
<target>A user with this name is already exisiting</target>
|
||||
</segment>
|
||||
|
@ -162,7 +162,7 @@
|
|||
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
|
||||
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
|
||||
</notes>
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>user.invalid_username</source>
|
||||
<target>The username must contain only letters, numbers, underscores, dots, pluses or minuses.</target>
|
||||
</segment>
|
||||
|
@ -171,7 +171,7 @@
|
|||
<notes>
|
||||
<note category="state" priority="1">obsolete</note>
|
||||
</notes>
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>validator.noneofitschild.self</source>
|
||||
<target>An element can not be its own parent.</target>
|
||||
</segment>
|
||||
|
@ -180,121 +180,121 @@
|
|||
<notes>
|
||||
<note category="state" priority="1">obsolete</note>
|
||||
</notes>
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>validator.noneofitschild.children</source>
|
||||
<target>You can not assign children element as parent (This would cause loops).</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ayNr6QK" name="validator.select_valid_category">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>validator.select_valid_category</source>
|
||||
<target>Please select a valid category!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="6vIlN5q" name="validator.part_lot.only_existing">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>validator.part_lot.only_existing</source>
|
||||
<target>Can not add new parts to this location as it is marked as "Only Existing"</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="3xoKOIS" name="validator.part_lot.location_full.no_increase">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>validator.part_lot.location_full.no_increase</source>
|
||||
<target>Location is full. Amount can not be increased (new value must be smaller than {{ old_amount }}).</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="R6Ov4Yt" name="validator.part_lot.location_full">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>validator.part_lot.location_full</source>
|
||||
<target>Location is full. Can not add new parts to it.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BNQk2e7" name="validator.part_lot.single_part">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>validator.part_lot.single_part</source>
|
||||
<target>This location can only contain a single part and it is already full!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="4gPskOG" name="validator.attachment.must_not_be_null">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>validator.attachment.must_not_be_null</source>
|
||||
<target>You must select an attachment type!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="cDDVrWT" name="validator.orderdetail.supplier_must_not_be_null">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>validator.orderdetail.supplier_must_not_be_null</source>
|
||||
<target>You must select an supplier!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="k5DDdB4" name="validator.measurement_unit.use_si_prefix_needs_unit">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>validator.measurement_unit.use_si_prefix_needs_unit</source>
|
||||
<target>To enable SI prefixes, you have to set a unit symbol!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="DuzIOCr" name="part.ipn.must_be_unique">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>part.ipn.must_be_unique</source>
|
||||
<target>The internal part number must be unique. {{ value }} is already in use!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Z4Kuuo2" name="validator.project.bom_entry.name_or_part_needed">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>validator.project.bom_entry.name_or_part_needed</source>
|
||||
<target>You have to choose a part for a part BOM entry or set a name for a non-part BOM entry.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="WF_v4ih" name="project.bom_entry.name_already_in_bom">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>project.bom_entry.name_already_in_bom</source>
|
||||
<target>There is already an BOM entry with this name!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="5v4p85H" name="project.bom_entry.part_already_in_bom">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>project.bom_entry.part_already_in_bom</source>
|
||||
<target>This part already exists in the BOM!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="3lM32Tw" name="project.bom_entry.mountnames_quantity_mismatch">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>project.bom_entry.mountnames_quantity_mismatch</source>
|
||||
<target>The number of mountnames has to match the BOMs quantity!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="x47D5WT" name="project.bom_entry.can_not_add_own_builds_part">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>project.bom_entry.can_not_add_own_builds_part</source>
|
||||
<target>You can not add a project's own builds part to the BOM.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="2x2XDI_" name="project.bom_has_to_include_all_subelement_parts">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>project.bom_has_to_include_all_subelement_parts</source>
|
||||
<target>The project BOM has to include all subprojects builds parts. Part %part_name% of project %project_name% missing!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="U9b1EzD" name="project.bom_entry.price_not_allowed_on_parts">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>project.bom_entry.price_not_allowed_on_parts</source>
|
||||
<target>Prices are not allowed on BOM entries associated with a part. Define the price on the part instead.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ID056SR" name="validator.project_build.lot_bigger_than_needed">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>validator.project_build.lot_bigger_than_needed</source>
|
||||
<target>You have selected more quantity to withdraw than needed! Remove unnecessary quantity.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="6hV5UqD" name="validator.project_build.lot_smaller_than_needed">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>validator.project_build.lot_smaller_than_needed</source>
|
||||
<target>You have selected less quantity to withdraw than needed for the build! Add additional quantity.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="G9ZKt.4" name="part.name.must_match_category_regex">
|
||||
<segment state="translated">
|
||||
<segment>
|
||||
<source>part.name.must_match_category_regex</source>
|
||||
<target>The part name does not match the regular expression stated by the category: %regex%</target>
|
||||
</segment>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue