This is an automated email from the ASF dual-hosted git repository.

yasith pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata.git


The following commit(s) were added to refs/heads/master by this push:
     new 6eac42a4e7 Support liking resources (#527)
6eac42a4e7 is described below

commit 6eac42a4e71325dd4c40d0baf6d919553a9dbbe6
Author: Ganning Xu <[email protected]>
AuthorDate: Sat Jun 21 05:04:36 2025 -0700

    Support liking resources (#527)
    
    * support liking resources
    
    * spotless
    
    * spotless
    
    * better loading animation
    
    * add license header
    
    * like -> star
    
    * spotless
    
    * use contructor based dependency injection
---
 modules/research-framework/portal/src/App.tsx      | 130 ++++++++-------
 .../portal/src/components/add/DatasetSearch.tsx    |   2 +-
 .../portal/src/components/add/RepoSearch.tsx       |  23 ++-
 .../portal/src/components/home/ResourceCard.tsx    |  36 +++-
 .../projects/AssociatedProjectsSection.tsx         |   4 +-
 .../components/resources/DeleteResourceButton.tsx  |  59 ++++---
 .../src/components/resources/ResourceDetails.tsx   |  32 +++-
 .../src/components/resources/ResourceOptions.tsx   |  88 ++++++++++
 .../components/resources/StarResourceButton.tsx    | 103 ++++++++++++
 .../components/resources/StarredResourcesPage.tsx  |  74 +++++++++
 .../portal/src/components/resources/index.tsx      |   4 +-
 .../portal/src/layouts/NavBar.tsx                  | 184 ++++++++++++---------
 .../portal/src/lib/controller.ts                   |  19 +++
 .../research/service/config/AuthzTokenFilter.java  |   2 +-
 .../service/controller/ResourceController.java     |  45 +++--
 .../research/service/handlers/ResourceHandler.java |  53 +++++-
 .../service/model/entity/ResourceStar.java         |  70 ++++++++
 .../service/model/repo/ResourceStarRepository.java |  39 +++++
 18 files changed, 780 insertions(+), 187 deletions(-)

diff --git a/modules/research-framework/portal/src/App.tsx 
b/modules/research-framework/portal/src/App.tsx
index 18d7036162..4df2957806 100644
--- a/modules/research-framework/portal/src/App.tsx
+++ b/modules/research-framework/portal/src/App.tsx
@@ -1,30 +1,47 @@
-import { useColorMode } from "./components/ui/color-mode";
-import { Route, Routes, useLocation, useNavigate } from "react-router";
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied. See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import {useColorMode} from "./components/ui/color-mode";
+import {Route, Routes, useLocation, useNavigate} from "react-router";
 import Home from "./components/home";
-import { Models } from "./components/models";
-import { Datasets } from "./components/datasets";
+import {Models} from "./components/models";
+import {Datasets} from "./components/datasets";
 import ResourceDetails from "./components/resources/ResourceDetails";
 import Notebooks from "./components/notebooks";
 import Repositories from "./components/repositories";
-import { Login } from "./components/auth/UserLoginPage";
+import {Login} from "./components/auth/UserLoginPage";
 import ProtectedComponent from "./components/auth/ProtectedComponent";
-import { AuthProvider, AuthProviderProps } from "react-oidc-context";
-import { useEffect, useState } from "react";
+import {AuthProvider, AuthProviderProps} from "react-oidc-context";
+import {useEffect, useState} from "react";
 import NavBarFooterLayout from "./layouts/NavBarFooterLayout";
-import { CybershuttleLanding } from "./components/home/CybershuttleLanding";
-import {
-  APP_REDIRECT_URI,
-  CLIENT_ID,
-  OPENID_CONFIG_URL,
-} from "./lib/constants";
-import { WebStorageStateStore } from "oidc-client-ts";
-import { Resources } from "./components/resources";
-import { UserSet } from "./components/auth/UserSet";
-import { Toaster } from "./components/ui/toaster";
-import { Events } from "./components/events";
-import { AddRepoMaster } from "./components/add/AddRepoMaster";
-import { Add } from "./components/add";
-import { AddProjectMaster } from "./components/add/AddProjectMaster";
+import {CybershuttleLanding} from "./components/home/CybershuttleLanding";
+import {APP_REDIRECT_URI, CLIENT_ID, OPENID_CONFIG_URL,} from 
"./lib/constants";
+import {WebStorageStateStore} from "oidc-client-ts";
+import {Resources} from "./components/resources";
+import {UserSet} from "./components/auth/UserSet";
+import {Toaster} from "./components/ui/toaster";
+import {Events} from "./components/events";
+import {AddRepoMaster} from "./components/add/AddRepoMaster";
+import {Add} from "./components/add";
+import {AddProjectMaster} from "./components/add/AddProjectMaster";
+import {StarredResourcesPage} from 
"@/components/resources/StarredResourcesPage.tsx";
+
 function App() {
   const colorMode = useColorMode();
   const navigate = useNavigate();
@@ -57,7 +74,7 @@ function App() {
             userinfo_endpoint: data.userinfo_endpoint,
             jwks_uri: data.jwks_uri,
           },
-          userStore: new WebStorageStateStore({ store: window.localStorage }),
+          userStore: new WebStorageStateStore({store: window.localStorage}),
           automaticSilentRenew: true,
         };
 
@@ -75,41 +92,42 @@ function App() {
   }
 
   return (
-    <>
-      <AuthProvider
-        {...oidcConfig}
-        onSigninCallback={() => {
-          navigate(location.search, { replace: true });
-        }}
-      >
-        <Toaster />
-        <UserSet />
-        <Routes>
-          {/* Public Route */}
-          <Route element={<NavBarFooterLayout />}>
-            <Route path="/" element={<CybershuttleLanding />} />
-            <Route path="/login" element={<Login />} />
-            <Route path="/resources" element={<Resources />} />
-            <Route path="/events" element={<Events />} />
-            <Route path="/resources/datasets" element={<Datasets />} />
-            <Route path="/resources/notebooks" element={<Notebooks />} />
-            <Route path="/resources/repositories" element={<Repositories />} />
-            <Route path="/resources/models" element={<Models />} />
-            <Route path="/resources/:type/:id" element={<ResourceDetails />} />
-          </Route>
+      <>
+        <AuthProvider
+            {...oidcConfig}
+            onSigninCallback={() => {
+              navigate(location.search, {replace: true});
+            }}
+        >
+          <Toaster/>
+          <UserSet/>
+          <Routes>
+            {/* Public Route */}
+            <Route element={<NavBarFooterLayout/>}>
+              <Route path="/" element={<CybershuttleLanding/>}/>
+              <Route path="/login" element={<Login/>}/>
+              <Route path="/resources" element={<Resources/>}/>
+              <Route path="/events" element={<Events/>}/>
+              <Route path="/resources/datasets" element={<Datasets/>}/>
+              <Route path="/resources/notebooks" element={<Notebooks/>}/>
+              <Route path="/resources/repositories" element={<Repositories/>}/>
+              <Route path="/resources/models" element={<Models/>}/>
+              <Route path="/resources/:type/:id" element={<ResourceDetails/>}/>
+            </Route>
 
-          {/* Protected Routes with Layout */}
-          <Route
-            element={<ProtectedComponent Component={NavBarFooterLayout} />}
-          >
-            <Route path="/sessions" element={<Home />} />
-            <Route path="/add" element={<Add />} />
-            <Route path="/add/repo" element={<AddRepoMaster />} />
-            <Route path="/add/project" element={<AddProjectMaster />} />
-          </Route>
-        </Routes>
-      </AuthProvider>
-    </>
+            {/* Protected Routes with Layout */}
+            <Route
+                element={<ProtectedComponent Component={NavBarFooterLayout}/>}
+            >
+              <Route path="/resources/starred" 
element={<StarredResourcesPage/>}/>
+              <Route path="/sessions" element={<Home/>}/>
+              <Route path="/add" element={<Add/>}/>
+              <Route path="/add/repo" element={<AddRepoMaster/>}/>
+              <Route path="/add/project" element={<AddProjectMaster/>}/>
+            </Route>
+          </Routes>
+        </AuthProvider>
+      </>
   );
 }
 
diff --git 
a/modules/research-framework/portal/src/components/add/DatasetSearch.tsx 
b/modules/research-framework/portal/src/components/add/DatasetSearch.tsx
index 9df9f97bbc..915f241890 100644
--- a/modules/research-framework/portal/src/components/add/DatasetSearch.tsx
+++ b/modules/research-framework/portal/src/components/add/DatasetSearch.tsx
@@ -39,7 +39,7 @@ export const DatasetSearchInput = ({
 
     const timeout = setTimeout(async () => {
       try {
-        const response = await api.get(`${CONTROLLER.resources}/search`, {
+        const response = await 
api.get(`${CONTROLLER.resources}/public/search`, {
           params: {
             type: ResourceTypeEnum.DATASET,
             name: datasetSearch,
diff --git 
a/modules/research-framework/portal/src/components/add/RepoSearch.tsx 
b/modules/research-framework/portal/src/components/add/RepoSearch.tsx
index 3f588a5d14..cc230b0d12 100644
--- a/modules/research-framework/portal/src/components/add/RepoSearch.tsx
+++ b/modules/research-framework/portal/src/components/add/RepoSearch.tsx
@@ -1,3 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied. See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
 /* eslint-disable @typescript-eslint/no-explicit-any */
 "use client";
 
@@ -41,7 +60,7 @@ const RepoSearchInput = ({
 
     const timeout = setTimeout(async () => {
       try {
-        const response = await api.get(`${CONTROLLER.resources}/search`, {
+        const response = await 
api.get(`${CONTROLLER.resources}/public/search`, {
           params: {
             type: ResourceTypeEnum.REPOSITORY,
             name: repoSearch,
@@ -85,7 +104,7 @@ const RepoSearchInput = ({
             </HStack>
           </Field.Root>
 
-          <ResourceCard size="sm" resource={selectedRepo} deletable={false}/>
+          <ResourceCard size="sm" resource={selectedRepo} deletable={false} 
removeOnUnStar={false}/>
         </>
     );
   }
diff --git 
a/modules/research-framework/portal/src/components/home/ResourceCard.tsx 
b/modules/research-framework/portal/src/components/home/ResourceCard.tsx
index 047ab3aa5e..6846a19098 100644
--- a/modules/research-framework/portal/src/components/home/ResourceCard.tsx
+++ b/modules/research-framework/portal/src/components/home/ResourceCard.tsx
@@ -1,3 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied. See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
 import {ModelResource, Resource} from "@/interfaces/ResourceType";
 import {Tag} from "@/interfaces/TagType";
 import {isValidImaage, resourceTypeToColor} from "@/lib/util";
@@ -5,18 +24,20 @@ import {Avatar, Badge, Box, Card, HStack, Image, Text,} from 
"@chakra-ui/react";
 import {ResourceTypeBadge} from "../resources/ResourceTypeBadge";
 import {ResourceTypeEnum} from "@/interfaces/ResourceTypeEnum";
 import {ModelCardButton} from "../models/ModelCardButton";
-import {DeleteResourceButton} from 
"@/components/resources/DeleteResourceButton.tsx";
 import {useState} from "react";
 import {Link} from 'react-router';
+import {ResourceOptions} from "@/components/resources/ResourceOptions.tsx";
 
 export const ResourceCard = ({
                                resource,
                                size = "sm",
-                               deletable = true
+                               deletable = true,
+                               removeOnUnStar = false,
                              }: {
   resource: Resource;
   size?: "sm" | "md" | "lg";
-  deletable?: boolean
+  deletable?: boolean;
+  removeOnUnStar?: boolean;
 }) => {
   const [hideCard, setHideCard] = useState(false);
   const author = resource.authors[0];
@@ -28,10 +49,14 @@ export const ResourceCard = ({
   const linkToWithType = `${resource.type}/${resource.id}`;
 
   const link = '/resources/' + linkToWithType;
-  const onDeleteSuccess = () => {
+  const hideCardCallback = () => {
     setHideCard(true);
   }
 
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const dummyOnUnStarSuccess = (_: string) => {
+  }
+
   const content = (
       <Card.Root
           overflow="hidden"
@@ -62,7 +87,8 @@ export const ResourceCard = ({
         <Card.Header>
           <HStack justifyContent={'space-between'} alignItems={'center'} 
flexWrap={'wrap'}>
             <Card.Title>{resource.name}</Card.Title>
-            {deletable && <DeleteResourceButton resource={resource} 
onSuccess={onDeleteSuccess}/>}
+            <ResourceOptions deleteable={deletable} resource={resource} 
onDeleteSuccess={hideCardCallback}
+                             onUnStarSuccess={removeOnUnStar ? 
hideCardCallback : dummyOnUnStarSuccess}/>
           </HStack>
         </Card.Header>
 
diff --git 
a/modules/research-framework/portal/src/components/projects/AssociatedProjectsSection.tsx
 
b/modules/research-framework/portal/src/components/projects/AssociatedProjectsSection.tsx
index ff8606952e..56b137fab1 100644
--- 
a/modules/research-framework/portal/src/components/projects/AssociatedProjectsSection.tsx
+++ 
b/modules/research-framework/portal/src/components/projects/AssociatedProjectsSection.tsx
@@ -7,7 +7,7 @@ import {ProjectCard} from "../home/ProjectCard";
 
 async function fetchProjects(id: string) {
   try {
-    const resp = await api.get(`${CONTROLLER.resources}/${id}/projects`);
+    const resp = await 
api.get(`${CONTROLLER.resources}/public/${id}/projects`);
     return resp.data;
   } catch (error) {
     console.error("Error fetching projects:", error);
@@ -38,7 +38,7 @@ export const AssociatedProjectsSection = ({
         <Heading fontWeight="bold" size="2xl" mb={2}>
           Associated Projects
         </Heading>
-        
+
         <SimpleGrid columns={{base: 1, md: 2}} gap={2}>
           {projects.map((project) => (
               <ProjectCard key={project.id} project={project}/>
diff --git 
a/modules/research-framework/portal/src/components/resources/DeleteResourceButton.tsx
 
b/modules/research-framework/portal/src/components/resources/DeleteResourceButton.tsx
index 580c4f5051..d4d5ba83bd 100644
--- 
a/modules/research-framework/portal/src/components/resources/DeleteResourceButton.tsx
+++ 
b/modules/research-framework/portal/src/components/resources/DeleteResourceButton.tsx
@@ -1,6 +1,23 @@
-import {Box, Button, CloseButton, Dialog, Input, Menu, Portal, Text, 
useDialog} from "@chakra-ui/react";
-import {BsThreeDots} from "react-icons/bs";
-import {FaTrash} from "react-icons/fa";
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied. See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import {Box, Button, CloseButton, Dialog, Input, Portal, Text, useDialog} from 
"@chakra-ui/react";
 import {Resource} from "@/interfaces/ResourceType.ts";
 import {useAuth} from "react-oidc-context";
 import {isResourceOwner} from "@/lib/util.ts";
@@ -8,6 +25,8 @@ import {useState} from "react";
 import api from "@/lib/api.ts";
 import {CONTROLLER} from "@/lib/controller.ts";
 import {toaster} from "@/components/ui/toaster.tsx";
+import {FaTrash} from "react-icons/fa";
+import {ResourceOptionButton} from 
"@/components/resources/ResourceOptions.tsx";
 
 export const DeleteResourceButton = ({
                                        resource,
@@ -48,27 +67,21 @@ export const DeleteResourceButton = ({
 
   return (
       <>
-        <Menu.Root>
-          <Menu.Trigger _hover={{
-            cursor: 'pointer',
-          }}>
-            <BsThreeDots/>
-          </Menu.Trigger>
-          <Menu.Positioner>
-            <Menu.Content>
-              <Menu.Item value={"delete"}
-                         color="fg.error"
-                         _hover={{bg: "bg.error", color: "fg.error", cursor: 
"pointer"}}
-                         onClick={() => dialog.setOpen(true)}
-              >
-                <FaTrash/>
-                <Box flex="1">Delete</Box>
-
-              </Menu.Item>
+        <ResourceOptionButton
+            gap={2}
+            color={'red.600'}
+            onClick={() => {
+              dialog.setOpen(true)
+            }}
+            _hover={{
+              cursor: 'pointer',
+              bg: 'red.200',
+            }}
+        >
+          <FaTrash/>
+          <Box>Delete</Box>
+        </ResourceOptionButton>
 
-            </Menu.Content>
-          </Menu.Positioner>
-        </Menu.Root>
 
         <Dialog.RootProvider size="sm" value={dialog}>
           <Portal>
diff --git 
a/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
 
b/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
index 80c5b17712..ae94b7bdd6 100644
--- 
a/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
+++ 
b/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
@@ -1,3 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied. See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
 import {useLocation, useNavigate, useParams} from "react-router";
 import {
   Avatar,
@@ -32,10 +51,10 @@ import {NotebookSpecificDetails} from 
"../notebooks/NotebookSpecificDetails";
 import {RepositorySpecificDetails} from 
"../repositories/RepositorySpecificDetails";
 import {CONTROLLER} from "@/lib/controller";
 import {DatasetSpecificDetails} from "../datasets/DatasetSpecificDetails";
-import {DeleteResourceButton} from 
"@/components/resources/DeleteResourceButton.tsx";
+import {ResourceOptions} from "@/components/resources/ResourceOptions.tsx";
 
 async function getResource(id: string) {
-  const response = await api.get(`${CONTROLLER.resources}/${id}`);
+  const response = await api.get(`${CONTROLLER.resources}/public/${id}`);
   return response.data;
 }
 
@@ -110,7 +129,14 @@ const ResourceDetails = () => {
                   {resource.name}
                 </Heading>
 
-                <DeleteResourceButton resource={resource} 
onSuccess={goToResources}/>
+                <ResourceOptions
+                    resource={resource}
+                    onDeleteSuccess={goToResources}
+                    deleteable={true}
+                    onUnStarSuccess={() => {
+                    }}
+                />
+
               </HStack>
 
               <HStack mt={2}>
diff --git 
a/modules/research-framework/portal/src/components/resources/ResourceOptions.tsx
 
b/modules/research-framework/portal/src/components/resources/ResourceOptions.tsx
new file mode 100644
index 0000000000..7fc54aa2fd
--- /dev/null
+++ 
b/modules/research-framework/portal/src/components/resources/ResourceOptions.tsx
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied. See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import {Resource} from "@/interfaces/ResourceType.ts";
+import {Button, ButtonProps, Menu, VStack} from "@chakra-ui/react";
+import {BsThreeDots} from "react-icons/bs";
+import {DeleteResourceButton} from 
"@/components/resources/DeleteResourceButton.tsx";
+import {StarResourceButton} from 
"@/components/resources/StarResourceButton.tsx";
+import {useAuth} from "react-oidc-context";
+
+export const ResourceOptions = ({resource, deleteable = true, onDeleteSuccess, 
onUnStarSuccess}:
+                                {
+                                  resource: Resource,
+                                  deleteable: boolean,
+                                  onDeleteSuccess: () => void,
+                                  onUnStarSuccess: (resourceId: string) => void
+                                }) => {
+
+  const auth = useAuth();
+  if (!auth.isAuthenticated) {
+    return null;
+  }
+
+  return (
+      <>
+        <Menu.Root>
+          <Menu.Trigger _hover={{
+            cursor: 'pointer',
+          }}>
+            <BsThreeDots/>
+          </Menu.Trigger>
+          <Menu.Positioner>
+            <Menu.Content>
+              <VStack gap={2} alignItems={'start'}>
+                <StarResourceButton resource={resource} 
onSuccess={onUnStarSuccess}/>
+                {deleteable && <DeleteResourceButton resource={resource} 
onSuccess={onDeleteSuccess}/>}
+              </VStack>
+            </Menu.Content>
+          </Menu.Positioner>
+        </Menu.Root>
+      </>
+  )
+}
+
+
+type ResourceOptionButtonProps = {
+  onClick: () => void;
+  children?: React.ReactNode;
+} & ButtonProps;
+
+export const ResourceOptionButton = ({
+                                       onClick,
+                                       children,
+                                       ...rest
+                                     }: ResourceOptionButtonProps) => {
+  return (
+      <Button
+          w="full"
+          transition="all .2s"
+          rounded="md"
+          gap={2}
+          onClick={onClick}
+          p={0}
+          display={'flex'}
+          justifyContent={'flex-start'}
+          bg={'transparent'}
+          {...rest}
+      >
+        {children}
+      </Button>
+  );
+};
diff --git 
a/modules/research-framework/portal/src/components/resources/StarResourceButton.tsx
 
b/modules/research-framework/portal/src/components/resources/StarResourceButton.tsx
new file mode 100644
index 0000000000..9e45b492f8
--- /dev/null
+++ 
b/modules/research-framework/portal/src/components/resources/StarResourceButton.tsx
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied. See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import {Resource} from "@/interfaces/ResourceType.ts";
+import {ResourceOptionButton} from 
"@/components/resources/ResourceOptions.tsx";
+import {Box} from "@chakra-ui/react";
+import api from "@/lib/api.ts";
+import {CONTROLLER} from "@/lib/controller.ts";
+import {toaster} from "@/components/ui/toaster.tsx";
+import {useEffect, useState} from "react";
+import {BsStar, BsStarFill} from "react-icons/bs";
+
+export const StarResourceButton = ({
+                                     resource,
+                                     onSuccess,
+                                   }: {
+  resource: Resource,
+  onSuccess: (resourceId: string) => void,
+}) => {
+  const [changeStarLoading, setChangeStarLoading] = useState(false);
+  const [initialLoad, setinitialLoad] = useState(false);
+  const [starred, setStarred] = useState(false);
+
+  useEffect(() => {
+    async function getWhetherUserStarred() {
+      setinitialLoad(true);
+      const resp = await 
api.get(`${CONTROLLER.resources}/${resource.id}/star`);
+      setStarred(resp.data);
+      setinitialLoad(false);
+    }
+
+    getWhetherUserStarred();
+  }, []);
+
+  const handleStarResource = async () => {
+    try {
+      setChangeStarLoading(true);
+      await api.post(`${CONTROLLER.resources}/${resource.id}/star`);
+      toaster.create({
+        title: starred ? "Unstarred" : "Starred",
+        description: resource.name,
+        type: "success",
+      })
+      setStarred(prev => {
+        if (prev) {
+          onSuccess(resource.id || "INVALID");
+        }
+        return !prev;
+      });
+    } catch {
+      toaster.create({
+        title: "Error liking resource",
+        type: "error",
+      });
+    } finally {
+      setChangeStarLoading(false);
+    }
+  }
+
+  if (initialLoad) {
+    return null;
+  }
+
+  return (
+      <>
+        <ResourceOptionButton
+            gap={2}
+            _hover={{
+              cursor: 'pointer',
+              bg: 'blue.200',
+            }}
+            color={'black'}
+            onClick={handleStarResource}
+            loading={changeStarLoading}
+        >
+          {
+            starred ? <BsStarFill color={'#EFBF04'}/> : <BsStar/>
+          }
+          <Box>
+            {
+              starred ? "Unstar" : "Star"
+            }
+          </Box>
+        </ResourceOptionButton>
+      </>
+  )
+}
\ No newline at end of file
diff --git 
a/modules/research-framework/portal/src/components/resources/StarredResourcesPage.tsx
 
b/modules/research-framework/portal/src/components/resources/StarredResourcesPage.tsx
new file mode 100644
index 0000000000..a81fd7987b
--- /dev/null
+++ 
b/modules/research-framework/portal/src/components/resources/StarredResourcesPage.tsx
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied. See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import {useEffect, useState} from "react";
+import {useAuth} from "react-oidc-context";
+import api from "@/lib/api.ts";
+import {CONTROLLER} from "@/lib/controller.ts";
+import {Container, SimpleGrid, Spinner} from "@chakra-ui/react";
+import {Resource} from "@/interfaces/ResourceType.ts";
+import {ResourceCard} from "@/components/home/ResourceCard.tsx";
+import {PageHeader} from "@/components/PageHeader.tsx";
+
+export const StarredResourcesPage = () => {
+  const [starredResources, setStarredResources] = useState([]);
+  const [loading, setLoading] = useState(false);
+  const auth = useAuth();
+
+  useEffect(() => {
+    if (auth.isLoading) return;
+
+    async function getStarredResources() {
+      setLoading(true);
+      const resp = await 
api.get(`${CONTROLLER.resources}/${auth.user?.profile.email}/stars`)
+      setStarredResources(resp.data);
+      setLoading(false);
+    }
+
+    getStarredResources();
+  }, [auth.isLoading]);
+
+  return (
+      <Container maxW="container.lg" mt={8}>
+        <PageHeader title={"Starred Resources"}
+                    description={"Resources that you have starred will show up 
here, for easy access."}/>
+        <SimpleGrid
+            columns={{base: 1, md: 2, lg: 4}}
+            mt={4}
+            gap={2}
+            justifyContent="space-around"
+        >
+          {starredResources.map((resource: Resource) => {
+            return (
+                <ResourceCard
+                    resource={resource}
+                    key={resource.id}
+                    removeOnUnStar={true}
+                />
+            );
+          })}
+        </SimpleGrid>
+        {
+            loading && (
+                <Spinner/>
+            )
+        }
+      </Container>
+  )
+}
\ No newline at end of file
diff --git 
a/modules/research-framework/portal/src/components/resources/index.tsx 
b/modules/research-framework/portal/src/components/resources/index.tsx
index a86bd1b626..74f064deac 100644
--- a/modules/research-framework/portal/src/components/resources/index.tsx
+++ b/modules/research-framework/portal/src/components/resources/index.tsx
@@ -29,7 +29,7 @@ const getResources = async (
     stringTagsArr: string[],
     searchText: string
 ) => {
-  const response = await api.get(`${CONTROLLER.resources}/`, {
+  const response = await api.get(`${CONTROLLER.resources}/public`, {
     params: {
       type: types.join(","),
       tag: stringTagsArr.join(","),
@@ -43,7 +43,7 @@ const getResources = async (
 
 const getTags = async () => {
   try {
-    const response = await api.get(`${CONTROLLER.resources}/tags/all`);
+    const response = await api.get(`${CONTROLLER.resources}/public/tags/all`);
     return response.data;
   } catch (error) {
     console.error("Error fetching:", error);
diff --git a/modules/research-framework/portal/src/layouts/NavBar.tsx 
b/modules/research-framework/portal/src/layouts/NavBar.tsx
index 7be0495c0d..ea53c19967 100644
--- a/modules/research-framework/portal/src/layouts/NavBar.tsx
+++ b/modules/research-framework/portal/src/layouts/NavBar.tsx
@@ -1,23 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied. See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
 import {
-  Text,
+  Box,
+  Button,
+  ButtonProps,
+  Collapsible,
   Flex,
-  Spacer,
-  Image,
   HStack,
-  Box,
   IconButton,
-  useDisclosure,
-  Collapsible,
-  Button,
+  Image,
+  Spacer,
   Stack,
-  ButtonProps,
+  Text,
+  useDisclosure,
 } from "@chakra-ui/react";
 import ApacheAiravataLogo from "../assets/airavata-logo.png";
-import { Link, useNavigate } from "react-router";
-import { RxHamburgerMenu } from "react-icons/rx";
-import { IoClose } from "react-icons/io5";
-import { UserMenu } from "@/components/auth/UserMenu";
-import { useAuth } from "react-oidc-context";
+import {Link, useNavigate} from "react-router";
+import {RxHamburgerMenu} from "react-icons/rx";
+import {IoClose} from "react-icons/io5";
+import {UserMenu} from "@/components/auth/UserMenu";
+import {useAuth} from "react-oidc-context";
 
 const NAV_CONTENT = [
   {
@@ -35,6 +54,11 @@ const NAV_CONTENT = [
     url: "/add",
     needsAuth: true,
   },
+  {
+    title: "Starred",
+    url: "/resources/starred",
+    needsAuth: true,
+  },
   {
     title: "Events",
     url: "/events",
@@ -64,7 +88,7 @@ interface NavLinkProps extends ButtonProps {
 }
 
 const NavBar = () => {
-  const { open, onToggle } = useDisclosure();
+  const {open, onToggle} = useDisclosure();
   const navigate = useNavigate();
   const auth = useAuth();
 
@@ -75,80 +99,80 @@ const NavBar = () => {
     return true; // Show all items that do not require authentication
   });
 
-  const NavLink = ({ title, url, ...props }: NavLinkProps) => (
-    <Button
-      variant="plain"
-      px={2}
-      _hover={{ bg: "gray.200" }}
-      onClick={() => {
-        navigate(url);
-        onToggle();
-      }}
-      {...props}
-    >
-      <Text color="gray.700" fontSize="md" textAlign="left">
-        {title}
-      </Text>
-    </Button>
+  const NavLink = ({title, url, ...props}: NavLinkProps) => (
+      <Button
+          variant="plain"
+          px={2}
+          _hover={{bg: "gray.200"}}
+          onClick={() => {
+            navigate(url);
+            onToggle();
+          }}
+          {...props}
+      >
+        <Text color="gray.700" fontSize="md" textAlign="left">
+          {title}
+        </Text>
+      </Button>
   );
 
   return (
-    <Box position="sticky" top="0" zIndex="1000" bg="white" boxShadow="sm">
-      <Flex align="center" p={4}>
-        {/* Hamburger Menu (Mobile Only) */}
-        <IconButton
-          aria-label="Toggle Navigation"
-          display={{ base: "inline-flex", md: "none" }}
-          onClick={onToggle}
-          variant="ghost"
-          mr={2}
-        >
-          {open ? <IoClose size={24} /> : <RxHamburgerMenu size={24} />}
-        </IconButton>
+      <Box position="sticky" top="0" zIndex="1000" bg="white" boxShadow="sm">
+        <Flex align="center" p={4}>
+          {/* Hamburger Menu (Mobile Only) */}
+          <IconButton
+              aria-label="Toggle Navigation"
+              display={{base: "inline-flex", md: "none"}}
+              onClick={onToggle}
+              variant="ghost"
+              mr={2}
+          >
+            {open ? <IoClose size={24}/> : <RxHamburgerMenu size={24}/>}
+          </IconButton>
 
-        {/* Logo */}
-        <Link to="/">
-          <Image src={ApacheAiravataLogo} alt="Logo" boxSize="30px" />
-        </Link>
+          {/* Logo */}
+          <Link to="/">
+            <Image src={ApacheAiravataLogo} alt="Logo" boxSize="30px"/>
+          </Link>
 
-        {/* Desktop Nav Links */}
-        <HStack ml={4} display={{ base: "none", md: "flex" }}>
-          {filteredNavContent.map((item) => (
-            <NavLink key={item.title} title={item.title} url={item.url} />
-          ))}
-        </HStack>
+          {/* Desktop Nav Links */}
+          <HStack ml={4} display={{base: "none", md: "flex"}}>
+            {filteredNavContent.map((item) => (
+                <NavLink key={item.title} title={item.title} url={item.url}/>
+            ))}
+          </HStack>
 
-        <Spacer />
+          <Spacer/>
 
-        {/* User Profile */}
-        <UserMenu />
-      </Flex>
+          {/* User Profile */}
+          <UserMenu/>
+        </Flex>
 
-      {/* Mobile Nav Links (Collapse) */}
-      <Collapsible.Root open={open}>
-        <Collapsible.Content>
-          <Stack
-            direction="column"
-            bg="white"
-            px={4}
-            pb={4}
-            spaceY={2}
-            display={{ md: "none" }}
-          >
-            {filteredNavContent.map((item) => (
-              <Box key={item.title} w="100%">
-                <NavLink
-                  key={item.title}
-                  title={item.title}
-                  url={item.url}
-                  width="100%"
-                />
-              </Box>
-            ))}
-          </Stack>
-        </Collapsible.Content>
-      </Collapsible.Root>
-    </Box>
+        {/* Mobile Nav Links (Collapse) */}
+        <Collapsible.Root open={open}>
+          <Collapsible.Content>
+            <Stack
+                direction="column"
+                bg="white"
+                px={4}
+                pb={4}
+                spaceY={2}
+                display={{md: "none"}}
+            >
+              {filteredNavContent.map((item) => (
+                  <Box key={item.title} w="100%">
+                    <NavLink
+                        key={item.title}
+                        title={item.title}
+                        url={item.url}
+                        width="100%"
+                    />
+                  </Box>
+              ))}
+            </Stack>
+          </Collapsible.Content>
+        </Collapsible.Root>
+      </Box>
   );
 };
 
diff --git a/modules/research-framework/portal/src/lib/controller.ts 
b/modules/research-framework/portal/src/lib/controller.ts
index 866e82e630..d4657b1877 100644
--- a/modules/research-framework/portal/src/lib/controller.ts
+++ b/modules/research-framework/portal/src/lib/controller.ts
@@ -1,3 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied. See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
 export const CONTROLLER = {
   projects: "/projects",
   hub: "/hub",
diff --git 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java
 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java
index 0fcd374cf5..5b459f8405 100644
--- 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java
+++ 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java
@@ -70,7 +70,7 @@ public class AuthzTokenFilter extends OncePerRequestFilter {
                 || path.startsWith("/swagger-ui")
                 || path.startsWith("/swagger-resources")
                 || path.startsWith("/webjars/")
-                || path.startsWith("/api/v1/rf/resources");
+                || path.startsWith("/api/v1/rf/resources/public");
     }
 
     @Override
diff --git 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResourceController.java
 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResourceController.java
index d5f2f696a1..d20de6bb65 100644
--- 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResourceController.java
+++ 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResourceController.java
@@ -37,7 +37,6 @@ import 
org.apache.airavata.research.service.model.entity.RepositoryResource;
 import org.apache.airavata.research.service.model.entity.Resource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Page;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.DeleteMapping;
@@ -57,11 +56,13 @@ public class ResourceController {
 
     private static final Logger LOGGER = 
LoggerFactory.getLogger(ResourceController.class);
 
-    @org.springframework.beans.factory.annotation.Autowired
-    private ResourceHandler resourceHandler;
+    private final ResourceHandler resourceHandler;
+    private final ProjectHandler projectHandler;
 
-    @Autowired
-    private ProjectHandler projectHandler;
+    public ResourceController(ResourceHandler resourceHandler, ProjectHandler 
projectHandler) {
+        this.resourceHandler = resourceHandler;
+        this.projectHandler = projectHandler;
+    }
 
     @PostMapping("/dataset")
     public ResponseEntity<ResourceResponse> createDatasetResource(@RequestBody 
DatasetResource datasetResource) {
@@ -96,13 +97,13 @@ public class ResourceController {
     }
 
     @Operation(summary = "Get all tags")
-    @GetMapping(value = "/tags/all")
+    @GetMapping(value = "/public/tags/all")
     public 
ResponseEntity<List<org.apache.airavata.research.service.model.entity.Tag>> 
getTags() {
         return ResponseEntity.ok(resourceHandler.getAllTagsByPopularity());
     }
 
     @Operation(summary = "Get dataset, notebook, repository, or model")
-    @GetMapping(value = "/{id}")
+    @GetMapping(value = "/public/{id}")
     public ResponseEntity<Resource> getResource(@PathVariable(value = "id") 
String id) {
         return ResponseEntity.ok(resourceHandler.getResourceById(id));
     }
@@ -114,7 +115,7 @@ public class ResourceController {
     }
 
     @Operation(summary = "Get all resources")
-    @GetMapping("/")
+    @GetMapping("/public")
     public ResponseEntity<Page<Resource>> getAllResources(
             @RequestParam(value = "pageNumber", defaultValue = "0") int 
pageNumber,
             @RequestParam(value = "pageSize", defaultValue = "10") int 
pageSize,
@@ -139,7 +140,7 @@ public class ResourceController {
     }
 
     @Operation(summary = "Get resource by name")
-    @GetMapping("/search")
+    @GetMapping("/public/search")
     public ResponseEntity<List<Resource>> searchResource(
             @RequestParam(value = "type") ResourceTypeEnum type,
             @RequestParam(value = "name", required = false) String name) {
@@ -149,7 +150,7 @@ public class ResourceController {
     }
 
     @Operation(summary = "Get projects associated with a resource")
-    @GetMapping(value = "/{id}/projects")
+    @GetMapping(value = "/public/{id}/projects")
     public ResponseEntity<List<Project>> 
getProjectsFromResourceId(@PathVariable(value = "id") String id) {
         Resource resouce = resourceHandler.getResourceById(id);
         List<Project> projects;
@@ -165,6 +166,30 @@ public class ResourceController {
         return ResponseEntity.ok(projects);
     }
 
+    @Operation(summary = "Star/unstar a resource")
+    @PostMapping(value = "/{id}/star")
+    public ResponseEntity<Boolean> starOrUnstarResource(@PathVariable(value = 
"id") String id) {
+        return ResponseEntity.ok(resourceHandler.starOrUnstarResource(id));
+    }
+
+    @Operation(summary = "Check whether a user star-ed a resource")
+    @GetMapping(value = "/{id}/star")
+    public ResponseEntity<Boolean> 
checkWhetherUserStarredResource(@PathVariable(value = "id") String id) {
+        return 
ResponseEntity.ok(resourceHandler.checkWhetherUserStarredResource(id));
+    }
+
+    @Operation(summary = "Get resource star count")
+    @GetMapping(value = "/resources/{id}/count")
+    public ResponseEntity<Long> getResourceStarCount(@PathVariable(value = 
"id") String id) {
+        return ResponseEntity.ok(resourceHandler.getResourceStarCount(id));
+    }
+
+    @Operation(summary = "Get all starred resources of a user")
+    @GetMapping(value = "/{userId}/stars")
+    public ResponseEntity<List<Resource>> 
getAllStarredResources(@PathVariable(value = "userId") String id) {
+        return ResponseEntity.ok(resourceHandler.getAllStarredResources(id));
+    }
+
     private Class<? extends Resource> getResourceType(ResourceTypeEnum 
resourceTypeEnum) {
         return switch (resourceTypeEnum) {
             case REPOSITORY -> RepositoryResource.class;
diff --git 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java
 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java
index 475e2b7c18..0e84a2b36f 100644
--- 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java
+++ 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java
@@ -36,9 +36,11 @@ import org.apache.airavata.research.service.enums.StatusEnum;
 import org.apache.airavata.research.service.model.UserContext;
 import org.apache.airavata.research.service.model.entity.RepositoryResource;
 import org.apache.airavata.research.service.model.entity.Resource;
+import org.apache.airavata.research.service.model.entity.ResourceStar;
 import org.apache.airavata.research.service.model.entity.Tag;
 import org.apache.airavata.research.service.model.repo.ProjectRepository;
 import org.apache.airavata.research.service.model.repo.ResourceRepository;
+import org.apache.airavata.research.service.model.repo.ResourceStarRepository;
 import org.apache.airavata.research.service.model.repo.TagRepository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -56,16 +58,19 @@ public class ResourceHandler {
     private final TagRepository tagRepository;
     private final ResourceRepository resourceRepository;
     private final ProjectRepository projectRepository;
+    private final ResourceStarRepository resourceStarRepository;
 
     public ResourceHandler(
             AiravataService airavataService,
             TagRepository tagRepository,
             ResourceRepository resourceRepository,
-            ProjectRepository projectRepository) {
+            ProjectRepository projectRepository,
+            ResourceStarRepository resourceStarRepository) {
         this.airavataService = airavataService;
         this.tagRepository = tagRepository;
         this.resourceRepository = resourceRepository;
         this.projectRepository = projectRepository;
+        this.resourceStarRepository = resourceStarRepository;
     }
 
     public void initializeResource(Resource resource) {
@@ -174,6 +179,50 @@ public class ResourceHandler {
         return resource;
     }
 
+    public boolean starOrUnstarResource(String resourceId) {
+        Resource resource = getResourceById(resourceId);
+        String userId = UserContext.userId();
+
+        List<ResourceStar> resourceStars = 
resourceStarRepository.findByResourceAndUserId(resource, userId);
+
+        if (resourceStars.isEmpty()) {
+            // user has not starred the resource yet
+            ResourceStar resourceStar = new ResourceStar();
+            resourceStar.setUserId(userId);
+            resourceStar.setResource(resource);
+            resourceStarRepository.save(resourceStar);
+        } else {
+            ResourceStar resourceStar = resourceStars.get(0);
+            resourceStarRepository.delete(resourceStar);
+        }
+
+        return true;
+    }
+
+    public boolean checkWhetherUserStarredResource(String resourceId) {
+        Resource resource = getResourceById(resourceId);
+        String userId = UserContext.userId();
+
+        return resourceStarRepository.existsByResourceAndUserId(resource, 
userId);
+    }
+
+    public List<Resource> getAllStarredResources(String userId) {
+        String loggedInUser = UserContext.userId();
+        if (!loggedInUser.equals(userId)) {
+            throw new RuntimeException(
+                    String.format("User %s is not authorized to request stars 
for %s", loggedInUser, userId));
+        }
+
+        List<ResourceStar> resourceStars =
+                
resourceStarRepository.findByUserIdAndResourceState(loggedInUser, 
StateEnum.ACTIVE);
+        return 
resourceStars.stream().map(ResourceStar::getResource).collect(Collectors.toList());
+    }
+
+    public long getResourceStarCount(String resourceId) {
+        Resource resource = getResourceById(resourceId);
+        return resourceStarRepository.countResourceStarByResource(resource);
+    }
+
     public Resource getResourceById(String id) {
         // Your logic to fetch the resource by ID
         Optional<Resource> opResource = 
resourceRepository.findByIdAndState(id, StateEnum.ACTIVE);
@@ -204,7 +253,7 @@ public class ResourceHandler {
         }
 
         resource.setState(StateEnum.DELETED);
-        resourceRepository.delete(resource);
+        resourceRepository.save(resource);
         return true;
     }
 
diff --git 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/ResourceStar.java
 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/ResourceStar.java
new file mode 100644
index 0000000000..e7ec4d7fc4
--- /dev/null
+++ 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/ResourceStar.java
@@ -0,0 +1,70 @@
+/**
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+package org.apache.airavata.research.service.model.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import org.hibernate.annotations.UuidGenerator;
+
+@Entity(name = "RESOURCE_STAR")
+public class ResourceStar {
+
+    @Id
+    @GeneratedValue
+    @UuidGenerator
+    @Column(nullable = false, updatable = false, length = 48)
+    private String id;
+
+    @Column(name = "user_id", nullable = false)
+    private String userId;
+
+    @ManyToOne(optional = false, fetch = FetchType.EAGER)
+    @JoinColumn(name = "resource_id", nullable = false)
+    private Resource resource;
+
+    public Resource getResource() {
+        return resource;
+    }
+
+    public void setResource(Resource resource) {
+        this.resource = resource;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+}
diff --git 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceStarRepository.java
 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceStarRepository.java
new file mode 100644
index 0000000000..267cdd9404
--- /dev/null
+++ 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceStarRepository.java
@@ -0,0 +1,39 @@
+/**
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+package org.apache.airavata.research.service.model.repo;
+
+import java.util.List;
+import org.apache.airavata.research.service.enums.StateEnum;
+import org.apache.airavata.research.service.model.entity.Resource;
+import org.apache.airavata.research.service.model.entity.ResourceStar;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface ResourceStarRepository extends JpaRepository<ResourceStar, 
String> {
+
+    boolean existsByResourceAndUserId(Resource resource, String ownerId);
+
+    List<ResourceStar> findByResourceAndUserId(Resource resource, String 
ownerId);
+
+    List<ResourceStar> findByUserIdAndResourceState(String loggedInUser, 
StateEnum stateEnum);
+
+    long countResourceStarByResource(Resource resource);
+}

Reply via email to