diff --git a/pom.xml b/pom.xml index 169f384..74191a9 100644 --- a/pom.xml +++ b/pom.xml @@ -72,6 +72,7 @@ test + io.quarkus quarkus-oidc @@ -81,6 +82,12 @@ quarkus-keycloak-authorization + + io.quarkus + quarkus-keycloak-admin-client-reactive + + + org.projectlombok lombok diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java index e71a738..546f8f8 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java @@ -21,6 +21,8 @@ public class ClubModel { @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; + String clubId; + String name; String country; diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java index a321d16..145b192 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java @@ -25,6 +25,8 @@ public class MembreModel { @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; + String userId; + String lname; String fname; diff --git a/src/main/java/fr/titionfire/ffsaf/domain/entity/ClubEntity.java b/src/main/java/fr/titionfire/ffsaf/domain/entity/ClubEntity.java index 45253dd..9fea3c8 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/entity/ClubEntity.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/entity/ClubEntity.java @@ -16,6 +16,7 @@ import java.util.Map; public class ClubEntity { private long id; private String name; + private String clubId; private String country; private String shieldURL; private Map contact; @@ -35,6 +36,7 @@ public class ClubEntity { return ClubEntity.builder() .id(model.getId()) .name(model.getName()) + .clubId(model.getClubId()) .country(model.getCountry()) .shieldURL(model.getShieldURL()) .contact(model.getContact()) @@ -49,7 +51,7 @@ public class ClubEntity { } public ClubModel toModel () { - return new ClubModel(this.id, this.name, this.country, this.shieldURL, this.contact, this.training_location, + return new ClubModel(this.id, this.clubId, this.name, this.country, this.shieldURL, this.contact, this.training_location, this.training_day_time, this.contact_intern, this.RNA, this.SIRET, this.no_affiliation, this.international); } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java index 9dab233..4169c52 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java @@ -32,4 +32,11 @@ public class ClubService { public Uni> getAll() { return repository.listAll(); } + + public Uni> setClubId(Long id, String id1) { + return repository.findById(id).chain(clubModel -> { + clubModel.setClubId(id1); + return Panache.withTransaction(() -> repository.persist(clubModel)); + }); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java new file mode 100644 index 0000000..4cbf7fe --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java @@ -0,0 +1,151 @@ +package fr.titionfire.ffsaf.domain.service; + +import fr.titionfire.ffsaf.data.model.ClubModel; +import fr.titionfire.ffsaf.data.model.MembreModel; +import fr.titionfire.ffsaf.utils.KeycloakException; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import io.vertx.mutiny.core.Vertx; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +import java.text.Normalizer; +import java.util.List; +import java.util.Optional; + +@ApplicationScoped +public class KeycloakService { + private static final Logger LOGGER = Logger.getLogger(KeycloakService.class); + + @Inject + Keycloak keycloak; + + @Inject + ClubService clubService; + + @Inject + MembreService membreService; + + @ConfigProperty(name = "keycloak.realm") + String realm; + + @Inject + Vertx vertx; + + public Uni getGroupFromClub(ClubModel club) { + if (club.getClubId() == null) { + LOGGER.infof("Creation of club group %d-%s...", club.getId(), club.getName()); + return vertx.getOrCreateContext().executeBlocking(() -> { + GroupRepresentation clubGroup = keycloak.realm(realm).groups().groups().stream().filter(g -> g.getName().equals("club")) + .findAny().orElseThrow(() -> new KeycloakException("Fail to fetch group %s".formatted("club"))); + + GroupRepresentation groupRepresentation = new GroupRepresentation(); + groupRepresentation.setName(club.getId() + "-" + club.getName()); + + try (Response response = keycloak.realm(realm).groups().group(clubGroup.getId()).subGroup(groupRepresentation)) { + if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo().equals(Response.Status.CONFLICT)) + throw new KeycloakException("Fail to set group parent for club: %s (reason=%s)".formatted(club.getName(), + response.getStatusInfo().getReasonPhrase())); + } + + return keycloak.realm(realm).groups().group(clubGroup.getId()).toRepresentation().getSubGroups().stream() + .filter(g -> g.getName().startsWith(club.getId() + "-")).findAny().map(GroupRepresentation::getId) + .orElseThrow(() -> new KeycloakException("Fail to fetch group %s*".formatted(club.getId() + "-"))); + } + ).call(id -> clubService.setClubId(club.getId(), id)); + } + return Uni.createFrom().item(club::getClubId); + } + + public Uni getUserFromMember(MembreModel membreModel) { + if (membreModel.getUserId() == null) { + return Uni.createFrom().failure(new NullPointerException("No keycloak user linked to the user id=" + membreModel.getId())); + } + return Uni.createFrom().item(membreModel::getUserId); + } + + public Uni setClubGroupMembre(MembreModel membreModel, ClubModel club) { + return getGroupFromClub(club).chain( + clubId -> getUserFromMember(membreModel).chain(userId -> vertx.getOrCreateContext().executeBlocking(() -> { + UserResource user = keycloak.realm(realm).users().get(userId); + user.groups().stream().filter(g -> g.getPath().startsWith("/club")).forEach(g -> user.leaveGroup(g.getId())); + user.joinGroup(clubId); + LOGGER.infof("Set club \"%s\" to user %s (%s)", club.getName(), userId, user.toRepresentation().getUsername()); + return "OK"; + }))); + } + + public Uni fetchCompte(String id) { + return vertx.getOrCreateContext().executeBlocking(() -> { + UserResource user = keycloak.realm(realm).users().get(id); + UserRepresentation user2 = user.toRepresentation(); + return new UserCompteState(user2.isEnabled(), user2.getUsername(), user2.isEmailVerified(), + user.roles().realmLevel().listEffective().stream().map(RoleRepresentation::getName).toList(), + user.groups().stream().map(GroupRepresentation::getName).toList()); + }); + } + + public Uni initCompte(long id) { + return membreService.getById(id).invoke(Unchecked.consumer(membreModel -> { + if (membreModel.getUserId() != null) + throw new KeycloakException("User already linked to the user id=" + id); + if (membreModel.getEmail() == null) + throw new KeycloakException("User email is null"); + if (membreModel.getFname() == null || membreModel.getLname() == null) + throw new KeycloakException("User name is null"); + })).chain(membreModel -> creatUser(membreModel).chain(user -> { + LOGGER.infof("Set user id %s to membre %s", user.getId(), membreModel.getId()); + return membreService.setUserId(membreModel.getId(), user.getId()); + })) + .map(__ -> "OK"); + } + + private Uni creatUser(MembreModel membreModel) { + String login = makeLogin(membreModel); + LOGGER.infof("Creation of user %s...", login); + return vertx.getOrCreateContext().executeBlocking(() -> { + UserRepresentation user = new UserRepresentation(); + user.setUsername(login); + user.setFirstName(membreModel.getFname()); + user.setLastName(membreModel.getLname()); + user.setEnabled(true); + + //user.setRequiredActions(List.of(UserModel.RequiredAction.VERIFY_EMAIL.name(), + // UserModel.RequiredAction.UPDATE_PASSWORD.name())); + + try (Response response = keycloak.realm(realm).users().create(user)) { + if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo().equals(Response.Status.CONFLICT)) + throw new KeycloakException("Fail to creat user %s (reason=%s)".formatted(login, response.getStatusInfo().getReasonPhrase())); + } + + return getUser(login).orElseThrow(() -> new KeycloakException("Fail to fetch user %s".formatted(login))); + }).call(user -> membreService.setUserId(membreModel.getId(), user.getId())); + } + + private Optional getUser(String username) { + List users = keycloak.realm(realm).users().searchByUsername(username, true); + + if (users.isEmpty()) + return Optional.empty(); + else + return Optional.of(users.get(0)); + } + + private String makeLogin(MembreModel model) { + return Normalizer.normalize((model.getFname().toLowerCase() + "." + model.getLname().toLowerCase()).replace(' ', '_'), Normalizer.Form.NFD) + .replaceAll("\\p{M}", ""); + + } + + public record UserCompteState(Boolean enabled, String login, Boolean emailVerified, List realmRoles, + List groups) { + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java index 7cb0f68..78d796d 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -29,6 +29,8 @@ public class MembreService { ClubRepository clubRepository; @Inject ServerCustom serverCustom; + @Inject + KeycloakService keycloakService; public SimpleCombModel find(int licence, String np) throws Throwable { return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> @@ -65,6 +67,15 @@ public class MembreService { return Panache.withTransaction(() -> repository.persist(m)); }) .invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, SimpleCombModel.fromModel(membreModel))) + .call(membreModel -> (membreModel.getUserId() != null) ? + keycloakService.setClubGroupMembre(membreModel, membreModel.getClub()) : Uni.createFrom().nullItem()) .map(__ -> "OK"); } + + public Uni> setUserId(Long id, String id1) { + return repository.findById(id).chain(membreModel -> { + membreModel.setUserId(id1); + return Panache.withTransaction(() -> repository.persist(membreModel)); + }); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/AuthEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/AuthEndpoints.java index aa52756..3371743 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/AuthEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/AuthEndpoints.java @@ -1,5 +1,6 @@ package fr.titionfire.ffsaf.rest; +import fr.titionfire.ffsaf.rest.data.UserInfo; import io.quarkus.security.Authenticated; import io.quarkus.security.identity.SecurityIdentity; import jakarta.inject.Inject; @@ -9,6 +10,7 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.jwt.JsonWebToken; import java.net.URI; import java.net.URISyntaxException; @@ -22,6 +24,9 @@ public class AuthEndpoints { @Inject SecurityIdentity securityIdentity; + @Inject + JsonWebToken accessToken; + @GET @Produces(MediaType.TEXT_PLAIN) public Boolean auth() { @@ -30,9 +35,10 @@ public class AuthEndpoints { @GET @Path("/userinfo") - @Produces(MediaType.TEXT_PLAIN) - public String userinfo() { - return securityIdentity.getPrincipal().getName(); + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public UserInfo userinfo() { + return UserInfo.makeUserInfo(accessToken, securityIdentity); } @GET diff --git a/src/main/java/fr/titionfire/ffsaf/rest/CompteEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/CompteEndpoints.java new file mode 100644 index 0000000..01c3440 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/CompteEndpoints.java @@ -0,0 +1,31 @@ +package fr.titionfire.ffsaf.rest; + +import fr.titionfire.ffsaf.domain.service.KeycloakService; +import io.smallrye.mutiny.Uni; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +@Path("api/compte") +public class CompteEndpoints { + + @Inject + KeycloakService service; + + @GET + @Path("{id}") + @RolesAllowed("federation_admin") + public Uni> getCompte(@PathParam("id") String id) { + return service.fetchCompte(id); + } + + @PUT + @Path("{id}/init") + @RolesAllowed("federation_admin") + public Uni> initCompte(@PathParam("id") long id) { + return service.initCompte(id); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleMembre.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleMembre.java index a553c36..dc51d65 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleMembre.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleMembre.java @@ -19,6 +19,7 @@ import java.util.Date; @RegisterForReflection public class SimpleMembre { private long id; + private String userId; private String lname = ""; private String fname = ""; private Categorie categorie; @@ -38,6 +39,7 @@ public class SimpleMembre { return new SimpleMembreBuilder() .id(model.getId()) + .userId(model.getUserId()) .lname(model.getLname()) .fname(model.getFname()) .categorie(model.getCategorie()) diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/UserInfo.java b/src/main/java/fr/titionfire/ffsaf/rest/data/UserInfo.java new file mode 100644 index 0000000..0c102f2 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/UserInfo.java @@ -0,0 +1,38 @@ +package fr.titionfire.ffsaf.rest.data; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import io.quarkus.security.identity.SecurityIdentity; +import lombok.Builder; +import lombok.Data; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.util.Set; + +@Data +@Builder +@RegisterForReflection +public class UserInfo { + String id; + String name; + String givenName; + String familyName; + String email; + boolean emailVerified; + long expiration; + Set groups; + Set roles; + + public static UserInfo makeUserInfo(JsonWebToken accessToken, SecurityIdentity securityIdentity) { + UserInfo.UserInfoBuilder builder = UserInfo.builder(); + builder.id(accessToken.getSubject()); + builder.name(accessToken.getName()); + builder.givenName(accessToken.getClaim("given_name")); + builder.familyName(accessToken.getClaim("family_name")); + builder.email(accessToken.getClaim("email")); + builder.emailVerified(accessToken.getClaim("email_verified")); + builder.expiration(accessToken.getExpirationTime()); + builder.groups(accessToken.getGroups()); + builder.roles(securityIdentity.getRoles()); + return builder.build(); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/utils/KeycloakException.java b/src/main/java/fr/titionfire/ffsaf/utils/KeycloakException.java new file mode 100644 index 0000000..f8a9673 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/utils/KeycloakException.java @@ -0,0 +1,8 @@ +package fr.titionfire.ffsaf.utils; + +public class KeycloakException extends Exception{ + + public KeycloakException(String msg) { + super(msg); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3d42376..053cf03 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,7 +17,8 @@ quarkus.quartz.start-mode=forced %dev.quarkus.log.min-level=ALL %dev.quarkus.log.category."fr.titionfire.ffsaf".level=ALL -quarkus.oidc.auth-server-url=https://auth.safca.fr/auth/realms/safca + +quarkus.oidc.auth-server-url=https://auth.safca.fr/realms/safca quarkus.oidc.client-id=backend quarkus.oidc.credentials.secret=secret quarkus.oidc.tls.verification=required @@ -35,6 +36,7 @@ database.pass= #Login quarkus.oidc.token-state-manager.split-tokens=true +quarkus.oidc.token.refresh-expired=true quarkus.oidc.authentication.redirect-path=/api/auth/login quarkus.oidc.logout.path=/api/logout @@ -46,4 +48,5 @@ quarkus.http.auth.permission.authenticated.policy=authenticated # All users can see the welcome page: quarkus.http.auth.permission.public.paths=/index.html -quarkus.http.auth.permission.public.policy=permit \ No newline at end of file +quarkus.http.auth.permission.public.policy=permit +quarkus.keycloak.admin-client.server-url=https://auth.safca.fr diff --git a/src/main/webapp/package-lock.json b/src/main/webapp/package-lock.json index d1c274e..37c5149 100644 --- a/src/main/webapp/package-lock.json +++ b/src/main/webapp/package-lock.json @@ -18,7 +18,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-loader-spinner": "^6.1.6", - "react-router-dom": "^6.21.2" + "react-router-dom": "^6.21.2", + "react-toastify": "^10.0.4" }, "devDependencies": { "@types/react": "^18.2.43", @@ -1631,6 +1632,14 @@ "node": ">=4" } }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -3609,6 +3618,18 @@ "react-dom": ">=16.8" } }, + "node_modules/react-toastify": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.4.tgz", + "integrity": "sha512-etR3RgueY8pe88SA67wLm8rJmL1h+CLqUGHuAoNsseW35oTGJEri6eBTyaXnFKNQ80v/eO10hBYLgz036XRGgA==", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", diff --git a/src/main/webapp/package.json b/src/main/webapp/package.json index 2b2667a..534a96f 100644 --- a/src/main/webapp/package.json +++ b/src/main/webapp/package.json @@ -20,7 +20,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-loader-spinner": "^6.1.6", - "react-router-dom": "^6.21.2" + "react-router-dom": "^6.21.2", + "react-toastify": "^10.0.4" }, "devDependencies": { "@types/react": "^18.2.43", diff --git a/src/main/webapp/src/App.jsx b/src/main/webapp/src/App.jsx index a8c8f61..04b7a97 100644 --- a/src/main/webapp/src/App.jsx +++ b/src/main/webapp/src/App.jsx @@ -1,5 +1,4 @@ import {useEffect, useRef} from 'react' -import './App.css' import {Nav} from "./components/Nav.jsx"; import {createBrowserRouter, Outlet, RouterProvider, useRouteError} from "react-router-dom"; import {Home} from "./pages/Homepage.jsx"; @@ -7,6 +6,10 @@ import {AdminRoot, getAdminChildren} from "./pages/admin/AdminRoot.jsx"; import {AuthCallback} from "./components/auhCallback.jsx"; import {KeycloakContextProvider, useAuthDispatch} from "./hooks/useAuth.jsx"; import {check_validity} from "./utils/auth.js"; +import {ToastContainer} from "react-toastify"; + +import './App.css' +import 'react-toastify/dist/ReactToastify.css'; const router = createBrowserRouter([ { @@ -49,7 +52,7 @@ function Root() { if (isInit.current) return; isInit.current = true - check_validity(b => dispatch({type: 'init', val: b})) + check_validity(data => dispatch({type: 'init', val: data})) }, []); return <> @@ -58,6 +61,19 @@ function Root() { + > } diff --git a/src/main/webapp/src/components/ColoredCircle.css b/src/main/webapp/src/components/ColoredCircle.css new file mode 100644 index 0000000..99abd95 --- /dev/null +++ b/src/main/webapp/src/components/ColoredCircle.css @@ -0,0 +1,9 @@ +div .colored-circle { + display: inline-block; + margin-left: 5px; + margin-right: 5px; + margin-bottom: -2px; + border-radius: 50%; + height: 20px; + width: 20px; +} \ No newline at end of file diff --git a/src/main/webapp/src/components/ColoredCircle.jsx b/src/main/webapp/src/components/ColoredCircle.jsx new file mode 100644 index 0000000..2f9d574 --- /dev/null +++ b/src/main/webapp/src/components/ColoredCircle.jsx @@ -0,0 +1,16 @@ +import {Fragment} from "react"; +import './ColoredCircle.css' + +export const ColoredCircle = ({color, boolean}) => { + const styles = {backgroundColor: '#F00'}; + + if (boolean === undefined) { + styles.backgroundColor = color + } else { + styles.backgroundColor = (boolean) ? '#00c700' : '#e50000'; + } + + return + + +}; \ No newline at end of file diff --git a/src/main/webapp/src/components/Nav.jsx b/src/main/webapp/src/components/Nav.jsx index d5eb0c9..3b95604 100644 --- a/src/main/webapp/src/components/Nav.jsx +++ b/src/main/webapp/src/components/Nav.jsx @@ -32,9 +32,9 @@ export function Nav() { } function AdminMenu() { - const {is_authenticated, data} = useAuth() + const {is_authenticated, userinfo} = useAuth() - if (!is_authenticated || !data?.realm_access?.roles?.includes("federation_admin")) + if (!is_authenticated || !userinfo?.roles?.includes("federation_admin")) return <>> return diff --git a/src/main/webapp/src/hooks/useAuth.jsx b/src/main/webapp/src/hooks/useAuth.jsx index acbc082..ba9e67f 100644 --- a/src/main/webapp/src/hooks/useAuth.jsx +++ b/src/main/webapp/src/hooks/useAuth.jsx @@ -25,15 +25,14 @@ function authReducer(auth, action) { switch (action.type) { case 'init': { return { - is_authenticated: action.val, - data: {realm_access: {roles: ["federation_admin"]}} - //data: action.val ? JSON.parse(atob(token.split('.')[1])) : null + is_authenticated: action.val.state, + userinfo: action.val.userinfo } } case 'update': { return { ...auth, - data: {realm_access: {roles: ["federation_admin"]}} + // data: {realm_access: {roles: ["federation_admin"]}} // data: JSON.parse(atob(action.token.split('.')[1])) } } @@ -51,5 +50,5 @@ function authReducer(auth, action) { const initialAuth = { is_authenticated: undefined, - data: undefined, + userinfo: undefined, } \ No newline at end of file diff --git a/src/main/webapp/src/pages/admin/MemberPage.jsx b/src/main/webapp/src/pages/admin/MemberPage.jsx index 24230c7..99e9770 100644 --- a/src/main/webapp/src/pages/admin/MemberPage.jsx +++ b/src/main/webapp/src/pages/admin/MemberPage.jsx @@ -1,11 +1,13 @@ import {useNavigate, useParams} from "react-router-dom"; import {useLoadingSwitcher} from "../../hooks/useLoading.jsx"; -import {useFetch} from "../../hooks/useFetch.js"; +import {useFetch, useFetchPut} from "../../hooks/useFetch.js"; import {AxiosError} from "../../components/AxiosError.jsx"; import {ClubSelect} from "../../components/ClubSelect.jsx"; import {useEffect, useState} from "react"; import {apiAxios, getCategoryFormBirthDate} from "../../utils/Tools.js"; import imageCompression from "browser-image-compression"; +import {ColoredCircle} from "../../components/ColoredCircle.jsx"; +import {toast} from "react-toastify"; const vite_url = import.meta.env.VITE_URL; @@ -41,9 +43,9 @@ export function MemberPage() { 'Content-Type': 'multipart/form-data', } }).then(data => { - console.log(data.data) + console.log(data.data) // TODO }).catch(e => { - console.log(e.response) + console.log(e.response) // TODO }).finally(() => { if (setLoading) setLoading(0) @@ -82,8 +84,18 @@ export function MemberPage() { > } - function MemberForm({data, handleSubmit}) { + const creatAccount = () => { + toast.promise( + apiAxios.put(`/compte/${data.id}/init`), + { + pending: 'Création du compte en cours', + success: 'Compte créé avec succès 🎉', + error: 'Échec de la création du compte 😕' + } + ) + } + return @@ -98,6 +110,25 @@ function MemberForm({data, handleSubmit}) { + + Compte + + {data.userId + ? + : <> + + + Ce membre ne dispose pas de compte... + + + + + Initialiser le compte + + + >} + + @@ -166,6 +197,38 @@ function MemberForm({data, handleSubmit}) { } +function CompteInfo({userId}) { + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/compte/${userId}`, setLoading, 1) + + return <> + {data + ? + : error && + } + > +} + +function CompteInfoContent({data}) { + return <> + + + Identifiant: {data.login} + + + + + Activer: + + + + + Email vérifié: + + + > +} + function BirthDayField({inti_date, inti_category}) { const [date, setDate] = useState(inti_date) const [category, setCategory] = useState(inti_category) diff --git a/src/main/webapp/src/utils/auth.js b/src/main/webapp/src/utils/auth.js index 9034362..0b12240 100644 --- a/src/main/webapp/src/utils/auth.js +++ b/src/main/webapp/src/utils/auth.js @@ -5,10 +5,13 @@ const vite_url = import.meta.env.VITE_URL; export function check_validity(online_callback = () => { }) { return axios.get(`${vite_url}/api/auth`).then(data => { - console.log(data.data) - online_callback(data.data); + if (data.data) { + axios.get(`${vite_url}/api/auth/userinfo`).then(data => { + online_callback({state: true, userinfo: data.data}); + }) + } }).catch(() => { - online_callback(false); + online_callback({state: false}); }) }