feat: add keycloak role configuration
This commit is contained in:
parent
e3306f4b5a
commit
8824a547bc
@ -13,6 +13,7 @@ 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.RoleScopeResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
@ -94,6 +95,21 @@ public class KeycloakService {
|
||||
});
|
||||
}
|
||||
|
||||
public Uni<List<String>> fetchRole(String id) {
|
||||
return vertx.getOrCreateContext().executeBlocking(() ->
|
||||
keycloak.realm(realm).users().get(id).roles().realmLevel().listEffective().stream().map(RoleRepresentation::getName).toList());
|
||||
}
|
||||
|
||||
public Uni<?> updateRole(String id, List<String> toAdd, List<String> toRemove) {
|
||||
return vertx.getOrCreateContext().executeBlocking(() -> {
|
||||
RoleScopeResource resource = keycloak.realm(realm).users().get(id).roles().realmLevel();
|
||||
List<RoleRepresentation> roles = keycloak.realm(realm) .roles().list();
|
||||
resource.add(roles.stream().filter(r -> toAdd.contains(r.getName())).toList());
|
||||
resource.remove(roles.stream().filter(r -> toRemove.contains(r.getName())).toList());
|
||||
return "OK";
|
||||
});
|
||||
}
|
||||
|
||||
public Uni<String> initCompte(long id) {
|
||||
return membreService.getById(id).invoke(Unchecked.consumer(membreModel -> {
|
||||
if (membreModel.getUserId() != null)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package fr.titionfire.ffsaf.rest;
|
||||
|
||||
import fr.titionfire.ffsaf.domain.service.KeycloakService;
|
||||
import fr.titionfire.ffsaf.rest.from.MemberPermForm;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
@ -9,6 +10,9 @@ import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Path("api/compte")
|
||||
public class CompteEndpoints {
|
||||
|
||||
@ -28,4 +32,30 @@ public class CompteEndpoints {
|
||||
public Uni<?> initCompte(@PathParam("id") long id) {
|
||||
return service.initCompte(id);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{id}/roles")
|
||||
@RolesAllowed("federation_admin")
|
||||
public Uni<?> getRole(@PathParam("id") String id) {
|
||||
return service.fetchRole(id);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("{id}/roles")
|
||||
@RolesAllowed("federation_admin")
|
||||
public Uni<?> updateRole(@PathParam("id") String id, MemberPermForm form) {
|
||||
List<String> toAdd = new ArrayList<>();
|
||||
List<String> toRemove = new ArrayList<>();
|
||||
|
||||
if (form.isFederation_admin()) toAdd.add("federation_admin");
|
||||
else toRemove.add("federation_admin");
|
||||
if (form.isSafca_super_admin()) toAdd.add("safca_super_admin");
|
||||
else toRemove.add("safca_super_admin");
|
||||
if (form.isSafca_user()) toAdd.add("safca_user");
|
||||
else toRemove.add("safca_user");
|
||||
if (form.isSafca_create_compet()) toAdd.add("safca_create_compet");
|
||||
else toRemove.add("safca_create_compet");
|
||||
|
||||
return service.updateRole(id, toAdd, toRemove);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
package fr.titionfire.ffsaf.rest.from;
|
||||
|
||||
import jakarta.ws.rs.FormParam;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class MemberPermForm {
|
||||
@FormParam("federation_admin")
|
||||
private boolean federation_admin;
|
||||
|
||||
@FormParam("safca_user")
|
||||
private boolean safca_user;
|
||||
|
||||
@FormParam("safca_create_compet")
|
||||
private boolean safca_create_compet;
|
||||
|
||||
@FormParam("safca_super_admin")
|
||||
private boolean safca_super_admin;
|
||||
}
|
||||
@ -1,13 +1,13 @@
|
||||
import {LoadingContextProvider, useLoadingSwitcher} from "../hooks/useLoading.jsx";
|
||||
import {LoadingProvider, useLoadingSwitcher} from "../hooks/useLoading.jsx";
|
||||
import {useFetch} from "../hooks/useFetch.js";
|
||||
import {AxiosError} from "./AxiosError.jsx";
|
||||
|
||||
export function ClubSelect({defaultValue, name}) {
|
||||
return <LoadingContextProvider>
|
||||
return <LoadingProvider>
|
||||
<div className="input-group mb-3">
|
||||
<ClubSelect_ defaultValue={defaultValue} name={name}/>
|
||||
</div>
|
||||
</LoadingContextProvider>
|
||||
</LoadingProvider>
|
||||
}
|
||||
|
||||
function ClubSelect_({defaultValue, name}) {
|
||||
|
||||
@ -13,7 +13,7 @@ export function useLoadingSwitcher() {
|
||||
return useContext(LoadingSwitcherContext);
|
||||
}
|
||||
|
||||
export function LoadingContextProvider({children}) {
|
||||
export function LoadingProvider({children}) {
|
||||
const [showOverlay, setOverlay] = useState(0);
|
||||
|
||||
return <LoadingContext.Provider value={showOverlay}>
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import {NavLink, Outlet} from "react-router-dom";
|
||||
import './AdminRoot.css'
|
||||
import {LoadingContextProvider} from "../../hooks/useLoading.jsx";
|
||||
import {LoadingProvider} from "../../hooks/useLoading.jsx";
|
||||
import {MemberList} from "./MemberList.jsx";
|
||||
import {MemberPage} from "./MemberPage.jsx";
|
||||
|
||||
export function AdminRoot() {
|
||||
return <>
|
||||
<h1>Espace administration</h1>
|
||||
<LoadingContextProvider>
|
||||
<LoadingProvider>
|
||||
<Outlet/>
|
||||
</LoadingContextProvider>
|
||||
</LoadingProvider>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {useNavigate, useParams} from "react-router-dom";
|
||||
import {useLoadingSwitcher} from "../../hooks/useLoading.jsx";
|
||||
import {LoadingProvider, useLoadingSwitcher} from "../../hooks/useLoading.jsx";
|
||||
import {useFetch, useFetchPut} from "../../hooks/useFetch.js";
|
||||
import {AxiosError} from "../../components/AxiosError.jsx";
|
||||
import {ClubSelect} from "../../components/ClubSelect.jsx";
|
||||
@ -18,63 +18,6 @@ export function MemberPage() {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/member/${id}`, setLoading, 1)
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(1)
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("id", data.id);
|
||||
formData.append("lname", event.target.lname?.value);
|
||||
formData.append("fname", event.target.fname?.value);
|
||||
formData.append("categorie", event.target.category?.value);
|
||||
formData.append("club", event.target.club?.value);
|
||||
formData.append("genre", event.target.genre?.value);
|
||||
formData.append("country", event.target.country?.value);
|
||||
formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString());
|
||||
formData.append("email", event.target.email?.value);
|
||||
formData.append("role", event.target.role?.value);
|
||||
formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
|
||||
|
||||
const send = (formData_) => {
|
||||
apiAxios.post(`/member/${id}`, formData_, {
|
||||
headers: {
|
||||
'Accept': '*/*',
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
}).then(data => {
|
||||
console.log(data.data) // TODO
|
||||
}).catch(e => {
|
||||
console.log(e.response) // TODO
|
||||
}).finally(() => {
|
||||
if (setLoading)
|
||||
setLoading(0)
|
||||
})
|
||||
}
|
||||
|
||||
const imageFile = event.target.url_photo.files[0];
|
||||
if (imageFile) {
|
||||
console.log(`originalFile size ${imageFile.size / 1024 / 1024} MB`);
|
||||
|
||||
const options = {
|
||||
maxSizeMB: 1,
|
||||
maxWidthOrHeight: 1920,
|
||||
useWebWorker: true,
|
||||
}
|
||||
|
||||
imageCompression(imageFile, options).then(compressedFile => {
|
||||
console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
|
||||
formData.append("photo_data", compressedFile)
|
||||
send(formData)
|
||||
});
|
||||
} else {
|
||||
send(formData)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmitPerm = (event) => {
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<h2>Page membre</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}>
|
||||
@ -85,11 +28,11 @@ export function MemberPage() {
|
||||
<div className="row">
|
||||
<div className="col-lg-4">
|
||||
<PhotoCard data={data}/>
|
||||
<CompteInfo userData={data}/>
|
||||
<LoadingProvider><CompteInfo userData={data}/></LoadingProvider>
|
||||
</div>
|
||||
<div className="col-lg-8">
|
||||
<InformationForm handleSubmit={handleSubmit} data={data}/>
|
||||
<PremForm handleSubmitPerm={handleSubmitPerm}/>
|
||||
<InformationForm data={data}/>
|
||||
<LoadingProvider><PremForm userData={data}/></LoadingProvider>
|
||||
<div className="row">
|
||||
<LicenceCard/>
|
||||
<SelectCard/>
|
||||
@ -116,7 +59,62 @@ function PhotoCard({data}) {
|
||||
</div>;
|
||||
}
|
||||
|
||||
function InformationForm({handleSubmit, data}) {
|
||||
function InformationForm({data}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(1)
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("id", data.id);
|
||||
formData.append("lname", event.target.lname?.value);
|
||||
formData.append("fname", event.target.fname?.value);
|
||||
formData.append("categorie", event.target.category?.value);
|
||||
formData.append("club", event.target.club?.value);
|
||||
formData.append("genre", event.target.genre?.value);
|
||||
formData.append("country", event.target.country?.value);
|
||||
formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString());
|
||||
formData.append("email", event.target.email?.value);
|
||||
formData.append("role", event.target.role?.value);
|
||||
formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
|
||||
|
||||
const send = (formData_) => {
|
||||
apiAxios.post(`/member/${data.id}`, formData_, {
|
||||
headers: {
|
||||
'Accept': '*/*',
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
}).then(data => {
|
||||
toast.success('Profile mis à jours avec succès 🎉');
|
||||
}).catch(e => {
|
||||
console.log(e.response)
|
||||
toast.error('Échec de la mise à jours du profile 😕 (code: ' + e.response.status + ')');
|
||||
}).finally(() => {
|
||||
if (setLoading)
|
||||
setLoading(0)
|
||||
})
|
||||
}
|
||||
|
||||
const imageFile = event.target.url_photo.files[0];
|
||||
if (imageFile) {
|
||||
console.log(`originalFile size ${imageFile.size / 1024 / 1024} MB`);
|
||||
|
||||
const options = {
|
||||
maxSizeMB: 1,
|
||||
maxWidthOrHeight: 1920,
|
||||
useWebWorker: true,
|
||||
}
|
||||
|
||||
imageCompression(imageFile, options).then(compressedFile => {
|
||||
console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
|
||||
formData.append("photo_data", compressedFile)
|
||||
send(formData)
|
||||
});
|
||||
} else {
|
||||
send(formData)
|
||||
}
|
||||
}
|
||||
|
||||
return <form onSubmit={handleSubmit}>
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Information</div>
|
||||
@ -161,31 +159,85 @@ function InformationForm({handleSubmit, data}) {
|
||||
</form>;
|
||||
}
|
||||
|
||||
function PremForm({handleSubmitPerm}) {
|
||||
function PremForm({userData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const handleSubmitPerm = (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(1)
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("federation_admin", event.target.federation_admin?.checked);
|
||||
formData.append("safca_user", event.target.safca_user?.checked);
|
||||
formData.append("safca_create_compet", event.target.safca_create_compet?.checked);
|
||||
formData.append("safca_super_admin", event.target.safca_super_admin?.checked);
|
||||
|
||||
apiAxios.put(`/compte/${userData.userId}/roles`, formData, {
|
||||
headers: {
|
||||
'Accept': '*/*',
|
||||
'Content-Type': 'form-data',
|
||||
}
|
||||
}).then(data => {
|
||||
toast.success('Permission mise à jours avec succès 🎉');
|
||||
}).catch(e => {
|
||||
console.log(e.response)
|
||||
toast.error('Échec de la mise à jours des permissions 😕 (code: ' + e.response.status + ')');
|
||||
}).finally(() => {
|
||||
if (setLoading)
|
||||
setLoading(0)
|
||||
})
|
||||
}
|
||||
|
||||
return <form onSubmit={handleSubmitPerm}>
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Permission</div>
|
||||
<div className="card-body">
|
||||
<div className="row g-3">
|
||||
<div className="col">
|
||||
<h5>FFSAF intra</h5>
|
||||
<CheckField name="federation_admin" text="Accès à l'intra" value={false}/>
|
||||
{userData.userId
|
||||
? <PremFormContent userData={userData}/>
|
||||
: <div className="col">
|
||||
<div className="input-group mb-3">
|
||||
<div>Ce membre ne dispose pas de compte...</div>
|
||||
</div>
|
||||
<div className="col">
|
||||
<h5>SAFCA</h5>
|
||||
<CheckField name="safca_user" text="Accès à l'application" value={false}/>
|
||||
<CheckField name="safca_create_compet" text="Créer des compétion" value={false}/>
|
||||
<CheckField name="safca_super_admin" text="Super administrateur" value={false}/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-12 text-right">
|
||||
<button type="submit" className="btn btn-primary">Enregistrer</button>
|
||||
{userData.userId && <button type="submit" className="btn btn-primary">Enregistrer</button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>;
|
||||
</form>
|
||||
}
|
||||
|
||||
function PremFormContent({userData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/compte/${userData.userId}/roles`, setLoading, 1)
|
||||
|
||||
return <>
|
||||
<div className="col">
|
||||
<h5>FFSAF intra</h5>
|
||||
{data
|
||||
? <>
|
||||
<CheckField name="federation_admin" text="Accès à l'intra"
|
||||
value={data.includes("federation_admin")}/>
|
||||
</>
|
||||
: error && <AxiosError error={error}/>}
|
||||
</div>
|
||||
<div className="col">
|
||||
<h5>SAFCA</h5>
|
||||
{data
|
||||
? <>
|
||||
<CheckField name="safca_user" text="Accès à l'application" value={data.includes("safca_user")}/>
|
||||
<CheckField name="safca_create_compet" text="Créer des compétion"
|
||||
value={data.includes("safca_create_compet")}/>
|
||||
<CheckField name="safca_super_admin" text="Super administrateur"
|
||||
value={data.includes("safca_super_admin")}/>
|
||||
</>
|
||||
: error && <AxiosError error={error}/>}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function LicenceCard() {
|
||||
@ -214,12 +266,15 @@ function SelectCard() {
|
||||
function CompteInfo({userData}) {
|
||||
|
||||
const creatAccount = () => {
|
||||
let err = {};
|
||||
toast.promise(
|
||||
apiAxios.put(`/compte/${userData.id}/init`),
|
||||
apiAxios.put(`/compte/${userData.id}/init`).catch(e => {
|
||||
err = e
|
||||
}),
|
||||
{
|
||||
pending: 'Création du compte en cours',
|
||||
success: 'Compte créé avec succès 🎉',
|
||||
error: 'Échec de la création du compte 😕'
|
||||
error: 'Échec de la création du compte 😕 (code: ' + err.response.status + ')'
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -229,7 +284,8 @@ function CompteInfo({userData}) {
|
||||
<div className="card-body text-center">
|
||||
{userData.userId
|
||||
? <CompteInfoContent userData={userData}/>
|
||||
: <>
|
||||
:
|
||||
<>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<div>Ce membre ne dispose pas de compte...</div>
|
||||
@ -240,13 +296,16 @@ function CompteInfo({userData}) {
|
||||
<button className="btn btn-primary" onClick={creatAccount}>Initialiser le compte</button>
|
||||
</div>
|
||||
</div>
|
||||
</>}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
function CompteInfoContent({userData}) {
|
||||
function CompteInfoContent({
|
||||
userData
|
||||
}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user