feat: add keycloak account creation and club groupe set

This commit is contained in:
Thibaut Valentin 2024-01-28 23:47:30 +01:00
parent d84ec9e1b4
commit 978c055834
22 changed files with 422 additions and 24 deletions

View File

@ -72,6 +72,7 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId> <artifactId>quarkus-oidc</artifactId>
@ -81,6 +82,12 @@
<artifactId>quarkus-keycloak-authorization</artifactId> <artifactId>quarkus-keycloak-authorization</artifactId>
</dependency> </dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-keycloak-admin-client-reactive</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>

View File

@ -21,6 +21,8 @@ public class ClubModel {
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id; Long id;
String clubId;
String name; String name;
String country; String country;

View File

@ -25,6 +25,8 @@ public class MembreModel {
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id; Long id;
String userId;
String lname; String lname;
String fname; String fname;

View File

@ -16,6 +16,7 @@ import java.util.Map;
public class ClubEntity { public class ClubEntity {
private long id; private long id;
private String name; private String name;
private String clubId;
private String country; private String country;
private String shieldURL; private String shieldURL;
private Map<Contact, String> contact; private Map<Contact, String> contact;
@ -35,6 +36,7 @@ public class ClubEntity {
return ClubEntity.builder() return ClubEntity.builder()
.id(model.getId()) .id(model.getId())
.name(model.getName()) .name(model.getName())
.clubId(model.getClubId())
.country(model.getCountry()) .country(model.getCountry())
.shieldURL(model.getShieldURL()) .shieldURL(model.getShieldURL())
.contact(model.getContact()) .contact(model.getContact())
@ -49,7 +51,7 @@ public class ClubEntity {
} }
public ClubModel toModel () { 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.training_day_time, this.contact_intern, this.RNA, this.SIRET, this.no_affiliation,
this.international); this.international);
} }

View File

@ -32,4 +32,11 @@ public class ClubService {
public Uni<List<ClubModel>> getAll() { public Uni<List<ClubModel>> getAll() {
return repository.listAll(); 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));
});
}
} }

View File

@ -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<String> 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<String> 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<String> 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<UserCompteState> 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<String> 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<UserRepresentation> 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<UserRepresentation> getUser(String username) {
List<UserRepresentation> 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<String> realmRoles,
List<String> groups) {
}
}

View File

@ -29,6 +29,8 @@ public class MembreService {
ClubRepository clubRepository; ClubRepository clubRepository;
@Inject @Inject
ServerCustom serverCustom; ServerCustom serverCustom;
@Inject
KeycloakService keycloakService;
public SimpleCombModel find(int licence, String np) throws Throwable { public SimpleCombModel find(int licence, String np) throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() ->
@ -65,6 +67,15 @@ public class MembreService {
return Panache.withTransaction(() -> repository.persist(m)); return Panache.withTransaction(() -> repository.persist(m));
}) })
.invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, SimpleCombModel.fromModel(membreModel))) .invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, SimpleCombModel.fromModel(membreModel)))
.call(membreModel -> (membreModel.getUserId() != null) ?
keycloakService.setClubGroupMembre(membreModel, membreModel.getClub()) : Uni.createFrom().nullItem())
.map(__ -> "OK"); .map(__ -> "OK");
} }
public Uni<?> setUserId(Long id, String id1) {
return repository.findById(id).chain(membreModel -> {
membreModel.setUserId(id1);
return Panache.withTransaction(() -> repository.persist(membreModel));
});
}
} }

View File

@ -1,5 +1,6 @@
package fr.titionfire.ffsaf.rest; package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.rest.data.UserInfo;
import io.quarkus.security.Authenticated; import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.SecurityIdentity;
import jakarta.inject.Inject; import jakarta.inject.Inject;
@ -9,6 +10,7 @@ import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.jwt.JsonWebToken;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
@ -22,6 +24,9 @@ public class AuthEndpoints {
@Inject @Inject
SecurityIdentity securityIdentity; SecurityIdentity securityIdentity;
@Inject
JsonWebToken accessToken;
@GET @GET
@Produces(MediaType.TEXT_PLAIN) @Produces(MediaType.TEXT_PLAIN)
public Boolean auth() { public Boolean auth() {
@ -30,9 +35,10 @@ public class AuthEndpoints {
@GET @GET
@Path("/userinfo") @Path("/userinfo")
@Produces(MediaType.TEXT_PLAIN) @Authenticated
public String userinfo() { @Produces(MediaType.APPLICATION_JSON)
return securityIdentity.getPrincipal().getName(); public UserInfo userinfo() {
return UserInfo.makeUserInfo(accessToken, securityIdentity);
} }
@GET @GET

View File

@ -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);
}
}

View File

@ -19,6 +19,7 @@ import java.util.Date;
@RegisterForReflection @RegisterForReflection
public class SimpleMembre { public class SimpleMembre {
private long id; private long id;
private String userId;
private String lname = ""; private String lname = "";
private String fname = ""; private String fname = "";
private Categorie categorie; private Categorie categorie;
@ -38,6 +39,7 @@ public class SimpleMembre {
return new SimpleMembreBuilder() return new SimpleMembreBuilder()
.id(model.getId()) .id(model.getId())
.userId(model.getUserId())
.lname(model.getLname()) .lname(model.getLname())
.fname(model.getFname()) .fname(model.getFname())
.categorie(model.getCategorie()) .categorie(model.getCategorie())

View File

@ -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<String> groups;
Set<String> 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();
}
}

View File

@ -0,0 +1,8 @@
package fr.titionfire.ffsaf.utils;
public class KeycloakException extends Exception{
public KeycloakException(String msg) {
super(msg);
}
}

View File

@ -17,7 +17,8 @@ quarkus.quartz.start-mode=forced
%dev.quarkus.log.min-level=ALL %dev.quarkus.log.min-level=ALL
%dev.quarkus.log.category."fr.titionfire.ffsaf".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.client-id=backend
quarkus.oidc.credentials.secret=secret quarkus.oidc.credentials.secret=secret
quarkus.oidc.tls.verification=required quarkus.oidc.tls.verification=required
@ -35,6 +36,7 @@ database.pass=
#Login #Login
quarkus.oidc.token-state-manager.split-tokens=true quarkus.oidc.token-state-manager.split-tokens=true
quarkus.oidc.token.refresh-expired=true
quarkus.oidc.authentication.redirect-path=/api/auth/login quarkus.oidc.authentication.redirect-path=/api/auth/login
quarkus.oidc.logout.path=/api/logout quarkus.oidc.logout.path=/api/logout
@ -46,4 +48,5 @@ quarkus.http.auth.permission.authenticated.policy=authenticated
# All users can see the welcome page: # All users can see the welcome page:
quarkus.http.auth.permission.public.paths=/index.html quarkus.http.auth.permission.public.paths=/index.html
quarkus.http.auth.permission.public.policy=permit quarkus.http.auth.permission.public.policy=permit
quarkus.keycloak.admin-client.server-url=https://auth.safca.fr

View File

@ -18,7 +18,8 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-loader-spinner": "^6.1.6", "react-loader-spinner": "^6.1.6",
"react-router-dom": "^6.21.2" "react-router-dom": "^6.21.2",
"react-toastify": "^10.0.4"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.43", "@types/react": "^18.2.43",
@ -1631,6 +1632,14 @@
"node": ">=4" "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": { "node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -3609,6 +3618,18 @@
"react-dom": ">=16.8" "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": { "node_modules/reflect.getprototypeof": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz",

View File

@ -20,7 +20,8 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-loader-spinner": "^6.1.6", "react-loader-spinner": "^6.1.6",
"react-router-dom": "^6.21.2" "react-router-dom": "^6.21.2",
"react-toastify": "^10.0.4"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.43", "@types/react": "^18.2.43",

View File

@ -1,5 +1,4 @@
import {useEffect, useRef} from 'react' import {useEffect, useRef} from 'react'
import './App.css'
import {Nav} from "./components/Nav.jsx"; import {Nav} from "./components/Nav.jsx";
import {createBrowserRouter, Outlet, RouterProvider, useRouteError} from "react-router-dom"; import {createBrowserRouter, Outlet, RouterProvider, useRouteError} from "react-router-dom";
import {Home} from "./pages/Homepage.jsx"; 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 {AuthCallback} from "./components/auhCallback.jsx";
import {KeycloakContextProvider, useAuthDispatch} from "./hooks/useAuth.jsx"; import {KeycloakContextProvider, useAuthDispatch} from "./hooks/useAuth.jsx";
import {check_validity} from "./utils/auth.js"; import {check_validity} from "./utils/auth.js";
import {ToastContainer} from "react-toastify";
import './App.css'
import 'react-toastify/dist/ReactToastify.css';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@ -49,7 +52,7 @@ function Root() {
if (isInit.current) if (isInit.current)
return; return;
isInit.current = true isInit.current = true
check_validity(b => dispatch({type: 'init', val: b})) check_validity(data => dispatch({type: 'init', val: data}))
}, []); }, []);
return <> return <>
@ -58,6 +61,19 @@ function Root() {
</header> </header>
<div className="container my-4"> <div className="container my-4">
<Outlet/> <Outlet/>
<ToastContainer
position="top-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="light"
transition: Flip
/>
</div> </div>
</> </>
} }

View File

@ -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;
}

View File

@ -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 <Fragment>
<span className="colored-circle" style={styles}/>
</Fragment>
};

View File

@ -32,9 +32,9 @@ export function Nav() {
} }
function AdminMenu() { 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 <></>
return <li className="nav-item dropdown"> return <li className="nav-item dropdown">

View File

@ -25,15 +25,14 @@ function authReducer(auth, action) {
switch (action.type) { switch (action.type) {
case 'init': { case 'init': {
return { return {
is_authenticated: action.val, is_authenticated: action.val.state,
data: {realm_access: {roles: ["federation_admin"]}} userinfo: action.val.userinfo
//data: action.val ? JSON.parse(atob(token.split('.')[1])) : null
} }
} }
case 'update': { case 'update': {
return { return {
...auth, ...auth,
data: {realm_access: {roles: ["federation_admin"]}} // data: {realm_access: {roles: ["federation_admin"]}}
// data: JSON.parse(atob(action.token.split('.')[1])) // data: JSON.parse(atob(action.token.split('.')[1]))
} }
} }
@ -51,5 +50,5 @@ function authReducer(auth, action) {
const initialAuth = { const initialAuth = {
is_authenticated: undefined, is_authenticated: undefined,
data: undefined, userinfo: undefined,
} }

View File

@ -1,11 +1,13 @@
import {useNavigate, useParams} from "react-router-dom"; import {useNavigate, useParams} from "react-router-dom";
import {useLoadingSwitcher} from "../../hooks/useLoading.jsx"; 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 {AxiosError} from "../../components/AxiosError.jsx";
import {ClubSelect} from "../../components/ClubSelect.jsx"; import {ClubSelect} from "../../components/ClubSelect.jsx";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {apiAxios, getCategoryFormBirthDate} from "../../utils/Tools.js"; import {apiAxios, getCategoryFormBirthDate} from "../../utils/Tools.js";
import imageCompression from "browser-image-compression"; 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; const vite_url = import.meta.env.VITE_URL;
@ -41,9 +43,9 @@ export function MemberPage() {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
} }
}).then(data => { }).then(data => {
console.log(data.data) console.log(data.data) // TODO
}).catch(e => { }).catch(e => {
console.log(e.response) console.log(e.response) // TODO
}).finally(() => { }).finally(() => {
if (setLoading) if (setLoading)
setLoading(0) setLoading(0)
@ -82,8 +84,18 @@ export function MemberPage() {
</> </>
} }
function MemberForm({data, handleSubmit}) { 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 <div> return <div>
<div className="row"> <div className="row">
<div className="col-lg-4"> <div className="col-lg-4">
@ -98,6 +110,25 @@ function MemberForm({data, handleSubmit}) {
</div> </div>
</div> </div>
</div> </div>
<div className="card mb-4">
<div className="card-header">Compte</div>
<div className="card-body text-center">
{data.userId
? <CompteInfo userId={data.userId}/>
: <>
<div className="row">
<div className="input-group mb-3">
<div>Ce membre ne dispose pas de compte...</div>
</div>
</div>
<div className="row">
<div className="input-group mb-3">
<button className="btn btn-primary" onClick={creatAccount}>Initialiser le compte</button>
</div>
</div>
</>}
</div>
</div>
</div> </div>
<div className="col-lg-8"> <div className="col-lg-8">
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
@ -166,6 +197,38 @@ function MemberForm({data, handleSubmit}) {
</div> </div>
} }
function CompteInfo({userId}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/compte/${userId}`, setLoading, 1)
return <>
{data
? <CompteInfoContent data={data}/>
: error && <AxiosError error={error}/>
}
</>
}
function CompteInfoContent({data}) {
return <>
<div className="row">
<div className="input-group mb-3">
<div>Identifiant: {data.login}</div>
</div>
</div>
<div className="row">
<div className="input-group mb-3">
<div>Activer: <ColoredCircle boolean={data.enabled}/></div>
</div>
</div>
<div className="row">
<div className="input-group mb-3">
<div>Email vérifié: <ColoredCircle boolean={data.emailVerified}/></div>
</div>
</div>
</>
}
function BirthDayField({inti_date, inti_category}) { function BirthDayField({inti_date, inti_category}) {
const [date, setDate] = useState(inti_date) const [date, setDate] = useState(inti_date)
const [category, setCategory] = useState(inti_category) const [category, setCategory] = useState(inti_category)

View File

@ -5,10 +5,13 @@ const vite_url = import.meta.env.VITE_URL;
export function check_validity(online_callback = () => { export function check_validity(online_callback = () => {
}) { }) {
return axios.get(`${vite_url}/api/auth`).then(data => { return axios.get(`${vite_url}/api/auth`).then(data => {
console.log(data.data) if (data.data) {
online_callback(data.data); axios.get(`${vite_url}/api/auth/userinfo`).then(data => {
online_callback({state: true, userinfo: data.data});
})
}
}).catch(() => { }).catch(() => {
online_callback(false); online_callback({state: false});
}) })
} }