1
0

align React-GUI and Java API -backend (#188)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/188
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig
2025-08-12 12:50:48 +02:00
parent 75f8a6a7db
commit 60028697d6
17 changed files with 212 additions and 91 deletions
@@ -19,6 +19,7 @@ import net.hostsharing.hsadminng.repr.Stringifyable;
import java.util.UUID;
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
import static net.hostsharing.hsadminng.repr.Symbol.symbol;
@Getter
@Setter
@@ -31,6 +32,10 @@ public abstract class HsCredentialsContext implements Stringifyable, BaseEntity<
private static Stringify<HsCredentialsContext> stringify = stringify(HsCredentialsContext.class, "loginContext")
.withProp(HsCredentialsContext::getType)
.withProp(HsCredentialsContext::getQualifier)
.withProp(HsCredentialsContext::isOnlyForNaturalPersons,
value -> value ? symbol("NP-ONLY") : null)
.withProp(HsCredentialsContext::isPublicAccess,
value -> value ? symbol("PUBLIC") : symbol("INTERNAL"))
.quotedValues(false)
.withSeparator(":");
@@ -53,6 +58,9 @@ public abstract class HsCredentialsContext implements Stringifyable, BaseEntity<
@Column(name = "only_for_natural_persons")
private boolean onlyForNaturalPersons;
@Column(name = "public_access")
private boolean publicAccess;
@Override
public String toShortString() {
return toString();
@@ -81,19 +81,15 @@ public class HsCredentialsController implements CredentialsApi {
@Override
@Transactional(readOnly = true)
@Timed("app.credentials.credentials.getListOfCredentialsByPersonUuid")
public ResponseEntity<List<CredentialsResource>> getListOfCredentialsByPersonUuid(
public ResponseEntity<List<CredentialsResource>> getListOfCredentials(
final String assumedRoles,
final UUID personUuid
) {
context.assumeRoles(assumedRoles);
final var person = rbacPersonRepo.findByUuid(personUuid).orElseThrow(
() -> new EntityNotFoundException(
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", personUuid)
)
);
final var credentials = credentialsRepo.findByPerson(person);
final var credentials = personUuid == null
? credentialsRepo.findByCurrentSubject()
: findByPersonUuid(personUuid);
final var result = mapper.mapList(
credentials, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(result);
@@ -183,6 +179,16 @@ public class HsCredentialsController implements CredentialsApi {
return subjectRepo.findByUuid(newRbacSubject.getUuid()); // attached to EM
}
private List<HsCredentialsEntity> findByPersonUuid(final UUID personUuid) {
final var person = rbacPersonRepo.findByUuid(personUuid).orElseThrow(
() -> new EntityNotFoundException(
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", personUuid)
)
);
return credentialsRepo.findByPerson(person);
}
final BiConsumer<HsCredentialsEntity, CredentialsResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
ofNullable(entity.getLastUsed()).ifPresent(
dt -> resource.setLastUsed(dt.atOffset(ZoneOffset.UTC)));
@@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.accounts;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import java.util.List;
@@ -14,7 +15,36 @@ public interface HsCredentialsRepository extends Repository<HsCredentialsEntity,
Optional<HsCredentialsEntity> findByUuid(final UUID uuid);
@Timed("app.login.credentials.repo.findByPerson")
List<HsCredentialsEntity> findByPerson(final HsOfficePerson personUuid);
List<HsCredentialsEntity> findByPerson(final HsOfficePerson<?> personUuid);
@Timed("app.login.credentials.repo.findByCurrentSubject")
@Query(nativeQuery = true, value = """
WITH RECURSIVE owned_persons AS (
-- Start with the person linked to current subject's credentials
SELECT p.uuid AS person_uuid
FROM hs_accounts.credentials c
JOIN hs_office.person p ON p.uuid = c.person_uuid
WHERE c.uuid = rbac.currentSubjectUuid()
UNION
-- Add persons where the current person has OWNER role
SELECT p.uuid AS person_uuid
FROM owned_persons op
CROSS JOIN hs_office.person p
WHERE rbac.isGranted(
rbac.currentSubjectUuid(),
rbac.findRoleId(
rbac.roleDescriptorOf('hs_office.person', p.uuid, 'OWNER'::rbac.RoleType, false)
)
)
)
SELECT DISTINCT c.*
FROM hs_accounts.credentials c
WHERE c.uuid = rbac.currentSubjectUuid() -- Include current subject's own credentials
OR c.person_uuid IN (SELECT person_uuid FROM owned_persons) -- Include credentials of owned persons
""")
List<HsCredentialsEntity> findByCurrentSubject();
@Timed("app.login.credentials.repo.save")
HsCredentialsEntity save(final HsCredentialsEntity entity);
@@ -1,5 +1,8 @@
package net.hostsharing.hsadminng.repr;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Value;
import net.hostsharing.hsadminng.errors.DisplayAs;
import jakarta.validation.constraints.NotNull;
@@ -11,14 +14,13 @@ import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import static java.lang.Boolean.TRUE;
import static java.util.Optional.ofNullable;
public final class Stringify<B> {
private final String name;
private Function<? extends B, ?> idProp;
private final List<Property<B>> props = new ArrayList<>();
private final List<Property<B, ?>> props = new ArrayList<>();
private String separator = ", ";
private Boolean quotedValues = null;
@@ -30,18 +32,6 @@ public final class Stringify<B> {
return new Stringify<>(clazz, null);
}
public <T extends B> Stringify<T> using(final Class<T> subClass) {
//noinspection unchecked
final var stringify = new Stringify<T>(subClass, null)
.withIdProp(cast(idProp))
.withProps(cast(props))
.withSeparator(separator);
if (quotedValues != null) {
stringify.quotedValues(quotedValues);
}
return stringify;
}
private Stringify(final Class<B> clazz, final String name) {
if (name != null) {
this.name = name;
@@ -55,31 +45,30 @@ public final class Stringify<B> {
}
}
public Stringify<B> withIdProp(final Function<? extends B, ?> getter) {
public <V> Stringify<B> withIdProp(final Function<? extends B, V> getter) {
idProp = getter;
return this;
}
public Stringify<B> withProp(final String propName, final Function<B, ?> getter) {
public <V> Stringify<B> withProp(final String propName, final Function<B, V> getter) {
props.add(new Property<>(propName, getter));
return this;
}
public Stringify<B> withProp(final Function<B, ?> getter) {
public <V> Stringify<B> withProp(final Function<B, V> getter) {
props.add(new Property<>(null, getter));
return this;
}
private Stringify<B> withProps(final List<Property<B>> props) {
this.props.addAll(props);
public <V> Stringify<B> withProp(final Function<B, V> getter, final Function<V, ?> mapper) {
props.add(new Property<>(null, getter, mapper));
return this;
}
public String apply(@NotNull B object) {
final var propValues = props.stream()
.map(prop -> PropertyValue.of(prop, prop.getter.apply(object)))
.filter(Objects::nonNull)
.filter(PropertyValue::nonEmpty)
.map(prop -> new PropertyValue<>(object, prop))
.filter(PropertyValue::notNullAndNotEmpty)
.map(propVal -> propName(propVal, "=") + optionallyQuoted(propVal))
.collect(Collectors.joining(separator));
return idProp != null
@@ -92,24 +81,36 @@ public final class Stringify<B> {
return this;
}
private String propName(final PropertyValue<B> propVal, final String delimiter) {
private <V> String propName(final PropertyValue<B, V> propVal, final String delimiter) {
return ofNullable(propVal.prop.name).map(v -> v + delimiter).orElse("");
}
private String optionallyQuoted(final PropertyValue<B> propVal) {
private <B, V> String optionallyQuoted(final PropertyValue<B, V> propVal) {
if (quotedValues == null)
return quotedQuotedValueType(propVal)
? ("'" + propVal.value + "'")
: propVal.value;
return TRUE == quotedValues
? ("'" + propVal.value + "'")
: propVal.value;
return quotableValueType(propVal.getValue())
? ("'" + propVal.stringValue + "'")
: propVal.stringValue;
return quotedValues
? ("'" + propVal.stringValue + "'")
: propVal.stringValue;
}
private static <B> boolean quotedQuotedValueType(final PropertyValue<B> propVal) {
return !(propVal.rawValue instanceof Number || propVal.rawValue instanceof Boolean);
private <V> boolean quotableValueType(final V rawValue) {
return !(rawValue instanceof Enum) &&
!(rawValue instanceof Symbol) &&
!(rawValue instanceof Number) &&
!(rawValue instanceof Boolean);
}
/**
* Specifies whether the values should be quoted (true) or not (false).
*
* If not specified, Enum, Symbol, Number and Boolean values are not quoted;
* other value types are quoted.
*
* @param quotedValues
* @return
*/
public Stringify<B> quotedValues(final boolean quotedValues) {
this.quotedValues = quotedValues;
return this;
@@ -120,23 +121,48 @@ public final class Stringify<B> {
return (T)object;
}
private record Property<B>(String name, Function<B, ?> getter) {}
@Value
@AllArgsConstructor
private class Property<B, V> {
String name;
Function<B, V> getter;
Function<V, ?> mapper; // FIXME: better generics?
private record PropertyValue<B>(Property<B> prop, Object rawValue, String value) {
static <B> PropertyValue<B> of(Property<B> prop, Object rawValue) {
return rawValue != null ? new PropertyValue<>(prop, rawValue, toStringOrShortString(rawValue)) : null;
Property(String name, Function<B, V> getter) {
this(name, getter, v -> v);
}
private static String toStringOrShortString(final Object rawValue) {
return rawValue instanceof Stringifyable stringifyable ? stringifyable.toShortString() : rawValue.toString();
Object getValue(final B object) {
return ofNullable(getter.apply(object))
.map(mapper)
.orElse(null);
}
}
@Getter
private class PropertyValue<B, V> {
private Property<B, V> prop;
private V value;
private String stringValue;
@SuppressWarnings("unchecked")
PropertyValue(final B object, final Property<B, ?> prop) {
// FIXME: simplify
final var typedProp = (Property<B, V>) prop;
final var value = typedProp.getValue(object);
final var stringifiedValue = value instanceof Stringifyable stringifyable
? stringifyable.toShortString()
: Objects.toString(value);
this.prop = typedProp;
this.value = (V) value;
this.stringValue = stringifiedValue;
}
boolean nonEmpty() {
return rawValue != null &&
(!(rawValue instanceof Collection<?> c) || !c.isEmpty()) &&
(!(rawValue instanceof Map<?,?> m) || !m.isEmpty()) &&
(!(rawValue instanceof String s) || !s.isEmpty());
boolean notNullAndNotEmpty() {
return value != null &&
(!(value instanceof Collection<?> c) || !c.isEmpty()) &&
(!(value instanceof Map<?,?> m) || !m.isEmpty()) &&
(!(value instanceof String s) || !s.isEmpty());
}
}
}
@@ -0,0 +1,25 @@
package net.hostsharing.hsadminng.repr;
import jakarta.validation.constraints.NotNull;
/**
* A String value which is used as a symbol and thus does not get quoted
* and is, by definition, different from any other symbol with the same name.
*/
public class Symbol {
private final String value;
public static Symbol symbol(@NotNull final String value) {
return new Symbol(value);
}
private Symbol(String value) {
this.value = value;
}
@Override
public String toString() {
return value;
}
}