Skip to content

Commit 23be28c

Browse files
committed
Add role based permissions for maintenance form generation and editing
1 parent 82c2444 commit 23be28c

11 files changed

Lines changed: 47 additions & 5 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
package com.apexgrid.transformertracker.auth;
22

3-
public record LoginResponse(String token, long expiresIn, String username, String image) { }
3+
public record LoginResponse(String token, long expiresIn, String username, String image, String role) { }

backend/src/main/java/com/apexgrid/transformertracker/model/User.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ public class User {
2525
@Column(name = "image", columnDefinition = "text")
2626
private String image;
2727

28+
@Column(name = "role")
29+
private String role;
30+
2831
public String getId() { return id; }
2932
public void setId(String id) { this.id = id; }
3033
public String getUsername() { return username; }
@@ -35,4 +38,6 @@ public class User {
3538
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
3639
public String getImage() { return image; }
3740
public void setImage(String image) { this.image = image; }
41+
public String getRole() { return role; }
42+
public void setRole(String role) { this.role = role; }
3843
}

backend/src/main/java/com/apexgrid/transformertracker/web/AuthController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) {
4646
token,
4747
jwtService.getExpirySeconds(),
4848
user.getUsername(),
49-
user.getImage() != null ? user.getImage() : ""
49+
user.getImage() != null ? user.getImage() : "",
50+
user.getRole() != null ? user.getRole() : ""
5051
);
5152
return ResponseEntity.ok(response);
5253
} catch (BadCredentialsException ex) {

frontend/app/inspections/[id]/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const InspectionDetailPage = () => {
2121
const [inspection, setInspection] = useState<Inspection | null>(null);
2222
const inspectionRef = useRef<Inspection | null>(null);
2323
const [username, setUsername] = useState<string | null>(null);
24+
const [userRole, setUserRole] = useState<string | null>(null);
2425
const [profileSrc, setProfileSrc] = useState<string>("/avatar.png");
2526
const [isLoading, setIsLoading] = useState(true);
2627
const [loadingMessage, setLoadingMessage] = useState<string>("Loading inspection...");
@@ -48,6 +49,7 @@ const InspectionDetailPage = () => {
4849
}
4950
if (typeof window !== "undefined") {
5051
setUsername(localStorage.getItem("username"));
52+
setUserRole(localStorage.getItem("userRole"));
5153
const stored = localStorage.getItem("userImage");
5254
if (stored && stored.length > 0) {
5355
setProfileSrc(stored);
@@ -183,6 +185,7 @@ const InspectionDetailPage = () => {
183185
<span className="text-sm text-gray-700">
184186
Logged in as:{" "}
185187
<span className="font-medium">{username || "unknown"}</span>
188+
{userRole && <span className="font-normal"> ({userRole})</span>}
186189
</span>
187190
<button
188191
onClick={() => router.push("/profile")}

frontend/app/inspections/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const InspectionsPage = () => {
2424
const [isEditOpen, setIsEditOpen] = useState(false);
2525
const [editingIndex, setEditingIndex] = useState<number | null>(null);
2626
const [username, setUsername] = useState<string | null>(null);
27+
const [userRole, setUserRole] = useState<string | null>(null);
2728
// Ensure client/server render the same initial avatar; update after mount from localStorage
2829
const [profileSrc, setProfileSrc] = useState<string>("/avatar.png");
2930

@@ -45,6 +46,7 @@ const InspectionsPage = () => {
4546
}
4647
if (typeof window !== "undefined") {
4748
setUsername(localStorage.getItem("username"));
49+
setUserRole(localStorage.getItem("userRole"));
4850
const stored = localStorage.getItem("userImage");
4951
if (stored && stored.length > 0) {
5052
setProfileSrc(stored);
@@ -151,6 +153,7 @@ const InspectionsPage = () => {
151153
<span className="text-sm text-gray-700">
152154
Logged in as:{" "}
153155
<span className="font-medium">{username || "unknown"}</span>
156+
{userRole && <span className="font-normal"> ({userRole})</span>}
154157
</span>
155158
<button
156159
onClick={() => router.push("/profile")}

frontend/app/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export default function Home() {
3737
image: data.image,
3838
token: data.token,
3939
expiresIn: data.expiresIn,
40+
role: data.role,
4041
},
4142
username
4243
);

frontend/app/profile/page.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import ThemeToggle from "@/components/ThemeToggle";
99
export default function ProfilePage() {
1010
const router = useRouter();
1111
const [username, setUsername] = useState<string | null>(null);
12+
const [userRole, setUserRole] = useState<string | null>(null);
1213
const [image, setImage] = useState<string | null>(null);
1314
const [hasPickedImage, setHasPickedImage] = useState(false);
1415
const [currentPassword, setCurrentPassword] = useState("");
@@ -27,6 +28,9 @@ export default function ProfilePage() {
2728
const u =
2829
typeof window !== "undefined" ? localStorage.getItem("username") : null;
2930
setUsername(u);
31+
const storedRole =
32+
typeof window !== "undefined" ? localStorage.getItem("userRole") : null;
33+
setUserRole(storedRole);
3034
fetch(apiUrl("/api/profile"), { headers: authHeaders() })
3135
.then((r) => (r.ok ? r.json() : Promise.reject(r)))
3236
.then((data) => {
@@ -147,7 +151,10 @@ export default function ProfilePage() {
147151
<div className="text-gray-700 dark:text-gray-300 text-sm">
148152
Logged in as
149153
</div>
150-
<div className="font-medium">{username}</div>
154+
<div className="font-medium">
155+
{username}
156+
{userRole && <span className="font-normal"> ({userRole})</span>}
157+
</div>
151158
</div>
152159
</div>
153160

frontend/app/transformer/[id]/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const TransformerDetailPage = () => {
3737

3838
const [transformer, setTransformer] = useState<Transformer | null>(null);
3939
const [username, setUsername] = useState<string | null>(null);
40+
const [userRole, setUserRole] = useState<string | null>(null);
4041
const [profileSrc, setProfileSrc] = useState<string>("/avatar.png");
4142
const [isLoading, setIsLoading] = useState(true);
4243
const [loadingMessage, setLoadingMessage] = useState<string>("Loading transformer...");
@@ -55,6 +56,7 @@ const TransformerDetailPage = () => {
5556
}
5657
if (typeof window !== "undefined") {
5758
setUsername(localStorage.getItem("username"));
59+
setUserRole(localStorage.getItem("userRole"));
5860
const stored = localStorage.getItem("userImage");
5961
if (stored && stored.length > 0) {
6062
setProfileSrc(stored);
@@ -240,6 +242,7 @@ const TransformerDetailPage = () => {
240242
<span className="text-sm text-gray-700">
241243
Logged in as:{" "}
242244
<span className="font-medium">{username || "unknown"}</span>
245+
{userRole && <span className="font-normal"> ({userRole})</span>}
243246
</span>
244247
<button
245248
onClick={() => router.push("/profile")}

frontend/app/transformer/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const TransformerPage = () => {
2424
const [isEditOpen, setIsEditOpen] = useState(false);
2525
const [editingIndex, setEditingIndex] = useState<number | null>(null);
2626
const [username, setUsername] = useState<string | null>(null);
27+
const [userRole, setUserRole] = useState<string | null>(null);
2728
// Preserve avatar between SSR and first client render
2829
const [profileSrc, setProfileSrc] = useState<string>("/avatar.png");
2930

@@ -46,6 +47,7 @@ const TransformerPage = () => {
4647
}
4748
if (typeof window !== "undefined") {
4849
setUsername(localStorage.getItem("username"));
50+
setUserRole(localStorage.getItem("userRole"));
4951
const stored = localStorage.getItem("userImage");
5052
if (stored && stored.length > 0) {
5153
setProfileSrc(stored);
@@ -148,6 +150,7 @@ const TransformerPage = () => {
148150
<span className="text-sm text-gray-700">
149151
Logged in as:{" "}
150152
<span className="font-medium">{username || "unknown"}</span>
153+
{userRole && <span className="font-normal"> ({userRole})</span>}
151154
</span>
152155
<button
153156
onClick={() => router.push("/profile")}

frontend/components/InspectionDetailsPanel.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,12 @@ const InspectionDetailsPanel = ({
465465
const [showMaintenanceModal, setShowMaintenanceModal] = useState(false);
466466
const [isSavingMaintenance, setIsSavingMaintenance] = useState(false);
467467
const [isReportGenerating, setIsReportGenerating] = useState(false);
468+
const [userRole, setUserRole] = useState<string | null>(null);
469+
470+
const isEngineer = useMemo(() => {
471+
if (!userRole) return false;
472+
return userRole.trim().toLowerCase() === "engineer";
473+
}, [userRole]);
468474

469475
const transformer = useMemo(
470476
() =>
@@ -707,6 +713,12 @@ const InspectionDetailsPanel = ({
707713
setPendingRect(null);
708714
};
709715

716+
useEffect(() => {
717+
if (typeof window !== "undefined") {
718+
setUserRole(localStorage.getItem("userRole"));
719+
}
720+
}, []);
721+
710722
// When switching to a different inspection, reinitialize weather and image state
711723
useEffect(() => {
712724
setTuneModelEnabled(true);
@@ -1695,9 +1707,9 @@ const InspectionDetailsPanel = ({
16951707
<button
16961708
type="button"
16971709
onClick={openMaintenanceForm}
1698-
disabled={maintenanceLoading}
1710+
disabled={maintenanceLoading || !isEngineer}
16991711
className="inline-flex items-center gap-2 px-3 py-1 text-sm border rounded custombutton disabled:opacity-60 disabled:cursor-not-allowed"
1700-
title="Generate or update a maintenance record for this inspection"
1712+
title={isEngineer ? "Generate or update a maintenance record for this inspection" : "Only engineers can generate or update maintenance records"}
17011713
>
17021714
<svg
17031715
className="w-4 h-4"

0 commit comments

Comments
 (0)