Skip to content

Commit b1d7af9

Browse files
committed
account-hooks endpoint adds example screener to account, synced frontend app loading to with account-hooks completion
1 parent 246e371 commit b1d7af9

9 files changed

Lines changed: 167 additions & 50 deletions

File tree

builder-api/src/main/java/org/acme/api/error/JsonServerExceptionMappers.java

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@ public class JsonServerExceptionMappers {
1515
public Response map(MismatchedInputException e) {
1616
Log.warn(e);
1717
// e.g. screenerName is object but DTO expects String
18-
String field =
19-
e.getPath() != null && !e.getPath().isEmpty()
20-
? e.getPath().get(e.getPath().size() - 1).getFieldName()
21-
: "request body";
18+
String field = e.getPath() != null && !e.getPath().isEmpty()
19+
? e.getPath().get(e.getPath().size() - 1).getFieldName()
20+
: "request body";
2221

2322
return Response.status(Response.Status.BAD_REQUEST)
2423
.type(MediaType.APPLICATION_JSON)
25-
.entity(ApiError.of("Invalid type for field '" + field + "'."))
26-
.build();
24+
.entity(ApiError.of("Invalid type for field '" + field + "'.")).build();
2725
}
2826

2927
@ServerExceptionMapper
@@ -32,16 +30,16 @@ public Response map(JsonParseException e) {
3230
// malformed JSON like { "schema": }
3331
return Response.status(Response.Status.BAD_REQUEST)
3432
.type(MediaType.APPLICATION_JSON)
35-
.entity(ApiError.of("Malformed JSON."))
36-
.build();
33+
.entity(ApiError.of("JsonParseException: Malformed JSON.")).build();
3734
}
3835

3936
@ServerExceptionMapper
4037
public Response map(WebApplicationException e) {
38+
Log.info("Some malformed JSON");
4139
Log.warn(e);
4240
return Response.status(Response.Status.BAD_REQUEST)
4341
.type(MediaType.APPLICATION_JSON)
44-
.entity(ApiError.of("Malformed JSON."))
42+
.entity(ApiError.of("WebApplicationException: Malformed JSON."))
4543
.build();
4644
}
4745

@@ -51,7 +49,6 @@ public Response map(JsonMappingException e) {
5149
// other mapping errors
5250
return Response.status(Response.Status.BAD_REQUEST)
5351
.type(MediaType.APPLICATION_JSON)
54-
.entity(ApiError.of("Invalid request body."))
55-
.build();
52+
.entity(ApiError.of("Invalid request body.")).build();
5653
}
5754
}

builder-api/src/main/java/org/acme/controller/AccountResource.java

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
package org.acme.controller;
22

3-
import io.quarkus.logging.Log;
43
import io.quarkus.security.identity.SecurityIdentity;
54
import jakarta.inject.Inject;
65
import jakarta.validation.Validator;
76
import jakarta.ws.rs.*;
87
import jakarta.ws.rs.core.*;
98

9+
import java.util.Map;
10+
import java.util.Set;
11+
import java.util.function.Predicate;
12+
import java.util.stream.Collectors;
13+
1014
import org.acme.api.error.ApiError;
1115
import org.acme.auth.AuthUtils;
16+
import org.acme.enums.AccountHookAction;
17+
import org.acme.functions.AccountHooks;
18+
import org.acme.model.dto.Auth.AccountHookRequest;
1219
import org.acme.model.dto.Auth.AccountHookResponse;
1320

1421
@Path("/api")
@@ -17,29 +24,40 @@ public class AccountResource {
1724
@Inject
1825
Validator validator;
1926

20-
@GET
27+
@Inject
28+
AccountHooks accountHooks;
29+
30+
@POST
31+
@Consumes(MediaType.APPLICATION_JSON)
2132
@Path("/account-hooks")
22-
public Response getScreeners(@Context SecurityIdentity identity,
23-
@QueryParam("action") String action) {
33+
public Response accountHooks(@Context SecurityIdentity identity,
34+
AccountHookRequest request) {
35+
36+
Set<AccountHookAction> hooks = request.hooks();
2437
String userId = AuthUtils.getUserId(identity);
38+
2539
if (userId == null) {
2640
return Response.status(Response.Status.UNAUTHORIZED)
2741
.entity(new ApiError(true, "Unauthorized.")).build();
2842
}
2943

30-
if (action == null || action.isBlank()) {
31-
return Response.status(Response.Status.BAD_REQUEST).entity(
32-
new ApiError(true, "Query parameter 'action' is required."))
33-
.build();
34-
}
35-
Log.info("Running account hooks for: " + userId);
44+
// Map of AccountHookAction to a hook side-effect function
45+
// The function returns whether the side-effect was successful
46+
Map<AccountHookAction, Predicate<String>> hooksMap = Map.of(
47+
AccountHookAction.ADD_EXAMPLE_SCREENER,
48+
accountHooks::addExampleScreenerToAccount,
49+
AccountHookAction.UNABLE_TO_DETERMINE,
50+
(String uId) -> true);
3651

37-
if (action.equals("add example screener")) {
38-
Log.info("***** Adding an exaample screener to the account *****");
39-
}
52+
// Run each action's function and determine whether successful
53+
Map<String, Boolean> hookResults = hooks.stream()
54+
.collect(Collectors.toMap(s -> s.toString(), s -> {
55+
Predicate<String> fn = hooksMap.get(s);
56+
return fn.test(userId);
57+
}));
4058

41-
AccountHookResponse responseBody = new AccountHookResponse(
42-
"add example screener", true);
59+
AccountHookResponse responseBody = new AccountHookResponse(true,
60+
hookResults);
4361

4462
return Response.ok(responseBody).build();
4563
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.acme.enums;
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonValue;
5+
6+
public enum AccountHookAction {
7+
ADD_EXAMPLE_SCREENER("add example screener"),
8+
UNABLE_TO_DETERMINE("UNABLE_TO_DETERMINE");
9+
10+
private final String label;
11+
12+
AccountHookAction(String label) {
13+
this.label = label;
14+
}
15+
16+
@JsonValue
17+
public String getLabel() {
18+
return label;
19+
}
20+
21+
@JsonCreator
22+
public static AccountHookAction fromValue(String value) {
23+
for (AccountHookAction action : values()) {
24+
if (action.label.equalsIgnoreCase(value)) {
25+
return action;
26+
}
27+
}
28+
return UNABLE_TO_DETERMINE;
29+
}
30+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.acme.functions;
2+
3+
import io.quarkus.logging.Log;
4+
import jakarta.enterprise.context.ApplicationScoped;
5+
import jakarta.inject.Inject;
6+
7+
import org.acme.model.domain.Screener;
8+
import org.acme.persistence.ScreenerRepository;
9+
10+
@ApplicationScoped
11+
public class AccountHooks {
12+
@Inject
13+
ScreenerRepository screenerRepository;
14+
15+
public Boolean addExampleScreenerToAccount(String userId) {
16+
try {
17+
Log.info("Running ADD_EXAMPLE_SCREENER hook for user: " + userId);
18+
String screenerName = "Example screener - Philly Property Tax Relief";
19+
String screenerDescription = "Example description";
20+
Screener exampleScreener = Screener
21+
.create(userId, screenerName, screenerDescription);
22+
23+
String screenerId = screenerRepository
24+
.saveNewWorkingScreener(exampleScreener);
25+
return true;
26+
} catch (Exception e) {
27+
Log.error(
28+
"Failed to run ADD_EXAMPLE_SCREENER hook for user: "
29+
+ userId,
30+
e);
31+
return false;
32+
}
33+
}
34+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.acme.model.dto.Auth;
2+
3+
import java.util.Set;
4+
5+
import org.acme.enums.AccountHookAction;
6+
7+
public record AccountHookRequest(Set<AccountHookAction> hooks) {
8+
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
package org.acme.model.dto.Auth;
22

3-
public record AccountHookResponse(String action, boolean success) {
3+
import java.util.Map;
4+
5+
public record AccountHookResponse(Boolean success,
6+
Map<String, Boolean> actions) {
47
};

builder-frontend/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import ProjectsList from "@/components/homeScreen/ProjectsList";
1616
import EligibilityChecksList from "@/components/homeScreen/eligibilityCheckList/EligibilityChecksList";
1717

1818
const MainLayout = (props: ParentProps) => {
19-
const { user, isAuthLoading } = useAuth();
19+
const { user, isAuthLoading, isProvisioningAccount } = useAuth();
2020

2121
const navbarItems = [
2222
{ label: "Projects", href: "/projects" },
@@ -25,7 +25,7 @@ const MainLayout = (props: ParentProps) => {
2525

2626
return (
2727
<Switch>
28-
<Match when={isAuthLoading()}>
28+
<Match when={isAuthLoading() || isProvisioningAccount()}>
2929
<Loading />
3030
</Match>
3131
<Match when={user() === null}>

builder-frontend/src/api/account.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import { env } from "@/config/environment";
22

3-
import { authGet } from "@/api/auth";
3+
import { authPost } from "@/api/auth";
44

55
const apiUrl = env.apiUrl;
66

77
export const getAccountHooks = async () => {
8-
const searchParams = new URLSearchParams([
9-
["action", "add example screener"],
10-
]);
11-
const accountHookUrl = new URL(
12-
`${apiUrl}/account-hooks?${searchParams.toString()}`,
13-
);
8+
const accountHookUrl = new URL(`${apiUrl}/account-hooks`);
9+
10+
const hooksToCall = ["add example screener"];
1411

1512
try {
16-
const response = await authGet(accountHookUrl.toString());
13+
const response = await authPost(accountHookUrl.toString(), {
14+
hooks: hooksToCall,
15+
});
1716

1817
if (!response.ok) {
1918
throw new Error(`Account hooks failed with status: ${response.status}`);

builder-frontend/src/context/AuthContext.jsx renamed to builder-frontend/src/context/AuthContext.tsx

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
} from "firebase/auth";
1616

1717
import { auth } from "../firebase/firebase";
18-
import { authGet } from "@/api/auth";
1918
import { getAccountHooks } from "@/api/account";
2019

2120
const AuthContext = createContext();
@@ -24,6 +23,7 @@ const googleProvider = new GoogleAuthProvider();
2423
export function AuthProvider(props) {
2524
const [user, setUser] = createSignal("loading");
2625
const [isAuthLoading, setIsAuthLoading] = createSignal(true);
26+
const [isProvisioningAccount, setIsProvisioningAccount] = createSignal(false);
2727
let unsubscribe;
2828

2929
onMount(() => {
@@ -43,25 +43,45 @@ export function AuthProvider(props) {
4343
};
4444

4545
const register = async (email, password) => {
46+
setIsProvisioningAccount(true);
4647
return createUserWithEmailAndPassword(auth, email, password).then(
4748
(userCredential) => {
48-
// Call "account creation hook" API endpoint here?
49-
console.log("***** after account creation hook *****");
50-
getAccountHooks().then(
51-
() => {
52-
console.log("Successfully hooked the account.");
53-
},
54-
(error) => {
55-
console.log("Error hooking the account", error);
56-
},
57-
);
49+
getAccountHooks()
50+
.then(
51+
() => {
52+
console.log("Successfully hooked the account.");
53+
},
54+
(error) => {
55+
console.log("Error hooking the account", error);
56+
},
57+
)
58+
.finally(() => {
59+
setIsProvisioningAccount(false);
60+
});
5861
},
5962
);
6063
};
6164

6265
const loginWithGoogle = async () => {
6366
try {
64-
return signInWithPopup(auth, googleProvider);
67+
return signInWithPopup(auth, googleProvider).then((userCredential) => {
68+
const { operationType } = userCredential;
69+
if (operationType === "link" || operationType === "reauthenticate") {
70+
setIsProvisioningAccount(true);
71+
getAccountHooks()
72+
.then(
73+
() => {
74+
console.log("Successfully hooked the account.");
75+
},
76+
(error) => {
77+
console.log("Error hooking the account", error);
78+
},
79+
)
80+
.finally(() => {
81+
setIsProvisioningAccount(false);
82+
});
83+
}
84+
});
6585
} catch (error) {
6686
console.error("Google sign-in error:", error.message);
6787
}
@@ -73,7 +93,15 @@ export function AuthProvider(props) {
7393

7494
return (
7595
<AuthContext.Provider
76-
value={{ user, isAuthLoading, login, register, loginWithGoogle, logout }}
96+
value={{
97+
user,
98+
isAuthLoading,
99+
isProvisioningAccount,
100+
login,
101+
register,
102+
loginWithGoogle,
103+
logout,
104+
}}
77105
>
78106
{props.children}
79107
</AuthContext.Provider>

0 commit comments

Comments
 (0)