Changeset b248810


Ignore:
Timestamp:
06/10/24 22:34:32 (5 months ago)
Author:
223021 <daniel.ilievski.2@…>
Branches:
main
Children:
befb988
Parents:
28b3398
Message:

Added no access page for new recruiters and admin panel for granting access

Files:
9 added
22 edited

Legend:

Unmodified
Added
Removed
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/JobvistaBackendApplication.java

    r28b3398 rb248810  
    2626                        admin.setRole(Role.ROLE_ADMIN);
    2727                        admin.setEmail("admin@admin.com");
     28                        admin.setHasAccess(true);
    2829//                      admin.setName("admin");
    2930//                      admin.setSurname("admin");
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/config/SecurityConfiguration.java

    r28b3398 rb248810  
    3232                        // TO DO: FIX PERMISSIONS
    3333                        .requestMatchers("/api/job-advertisements/**","/api/job-advertisements/view/**","/api/recruiter/info/**",
    34                                 "/api/job-advertisements/apply/**","/api/auth/**", "/api/resume/**", "/api/my-applications/**", "/api/applications/{id}/update").permitAll()
     34                                "/api/job-advertisements/apply/**","/api/auth/**", "/api/resume/**", "/api/my-applications/**", "/api/applications/{id}/update", "/api/admin/**").permitAll()
    3535                        //.requestMatchers("/api/job-advertisements/**").hasAnyAuthority(Role.ROLE_RECRUITER.name())
    3636                        .anyRequest().authenticated())
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/controllers/AdminController.java

    r28b3398 rb248810  
    22
    33import lombok.RequiredArgsConstructor;
    4 import org.springframework.web.bind.annotation.RequestMapping;
    5 import org.springframework.web.bind.annotation.RestController;
     4import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDetailsDTO;
     5import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.AdminService;
     6import org.springframework.http.HttpStatus;
     7import org.springframework.http.ResponseEntity;
     8import org.springframework.web.bind.annotation.*;
     9
     10import java.util.List;
    611
    712@RestController
    813@RequestMapping("/api/admin")
    914@RequiredArgsConstructor
     15@CrossOrigin(origins = "*")
    1016public class AdminController {
    1117
     18    private final AdminService adminService;
     19
     20    @PostMapping("/change-access/{recruiter_id}")
     21    public ResponseEntity<?> changeAccess(@PathVariable("recruiter_id") Long recruiterId, @RequestBody boolean access) {
     22        RecruiterDetailsDTO recruiterDetailsDTO = adminService.changeAccess(recruiterId, access);
     23        return new ResponseEntity<>(recruiterDetailsDTO, HttpStatus.OK);
     24    }
     25
     26    @GetMapping("/recruiters")
     27    public ResponseEntity<?> findAllRecruiters() {
     28        List<RecruiterDetailsDTO> recruiterDetailsDTOList = adminService.findAllRecruiters();
     29        return new ResponseEntity<>(recruiterDetailsDTOList, HttpStatus.OK);
     30    }
    1231}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/users/DTO/JwtAuthResponse.java

    r28b3398 rb248810  
    1414    private String name;
    1515    private String role;
     16    private boolean hasAccess;
    1617    private String token;
    1718    private String refreshToken;
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/users/DTO/RecruiterDetailsDTO.java

    r28b3398 rb248810  
    55import lombok.NoArgsConstructor;
    66
     7import java.time.LocalDateTime;
     8
    79@Data
    810@AllArgsConstructor
    911@NoArgsConstructor
    1012public class RecruiterDetailsDTO {
     13    private Long id;
    1114    private String email;
    1215    private String companyName;
    1316    private String companyDescription;
    1417    private String phoneNumber;
     18    private boolean hasAccess;
     19    private LocalDateTime registeredOn;
    1520}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/users/User.java

    r28b3398 rb248810  
    1111import org.springframework.security.core.userdetails.UserDetails;
    1212
     13import java.time.LocalDateTime;
    1314import java.util.Collection;
    1415import java.util.List;
     
    3233    @Enumerated(EnumType.STRING)
    3334    protected Role role;
     35
     36    @Column(name = "has_access")
     37    protected boolean hasAccess;
     38
     39    protected LocalDateTime registeredOn;
    3440
    3541
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/users/mappers/RecruiterMapper.java

    r28b3398 rb248810  
    1818    public static RecruiterDetailsDTO mapToRecruiterDetailsDTO(Recruiter recruiter) {
    1919        return new RecruiterDetailsDTO(
     20                recruiter.getId(),
    2021                recruiter.getEmail(),
    2122                recruiter.getCompanyName(),
    2223                recruiter.getCompanyDescription(),
    23                 recruiter.getPhoneNumber()
     24                recruiter.getPhoneNumber(),
     25                recruiter.isHasAccess(),
     26                recruiter.getRegisteredOn()
    2427        );
    2528    }
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/impl/AuthServiceImpl.java

    r28b3398 rb248810  
    1818import org.springframework.stereotype.Service;
    1919
     20import java.time.LocalDateTime;
    2021import java.util.HashMap;
    2122
     
    3435    public User signUpJobSeeker(JobSeeker jobSeeker) {
    3536        jobSeeker.setPassword(passwordEncoder.encode(jobSeeker.getPassword()));
     37        jobSeeker.setHasAccess(true);
     38        jobSeeker.setRegisteredOn(LocalDateTime.now());
    3639        return jobSeekerRepository.save(jobSeeker);
    3740    }
     
    3942    public User signUpRecruiter(Recruiter recruiter) {
    4043        recruiter.setPassword(passwordEncoder.encode(recruiter.getPassword()));
     44        recruiter.setHasAccess(false);
     45        recruiter.setRegisteredOn(LocalDateTime.now());
    4146        return recruiterRepository.save(recruiter);
    4247    }
     
    4954        String refreshJwt = jwtService.generateRefreshToken(new HashMap<>(), user);
    5055
    51         return new JwtAuthResponse(user.getId(), user.getEmail(), user.getName(), user.getRole().name(), jwt, refreshJwt);
     56        return new JwtAuthResponse(user.getId(), user.getEmail(), user.getName(), user.getRole().name(), user.isHasAccess(), jwt, refreshJwt);
    5257    }
    5358   
     
    5863            String jwt = jwtService.generateToken(user);
    5964
    60             return new JwtAuthResponse(user.getId(), user.getEmail(), user.getName(), user.getRole().name(), jwt, refreshTokenRequest.getToken());
     65            return new JwtAuthResponse(user.getId(), user.getEmail(), user.getName(), user.getRole().name(), user.isHasAccess(), jwt, refreshTokenRequest.getToken());
    6166        }
    6267        return null;
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/impl/JwtServiceImpl.java

    r28b3398 rb248810  
    66import io.jsonwebtoken.io.Decoders;
    77import io.jsonwebtoken.security.Keys;
     8import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.User;
    89import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.JwtService;
    910import org.springframework.security.core.userdetails.UserDetails;
     
    1920    private final static String SECRET_KEY = "7191b1d33668d4a2316a02f9a40798b77bccd22173bd882c93a0a916a5e921d1";
    2021
    21     public String generateToken(UserDetails userDetails) {
    22         return Jwts.builder().setSubject(userDetails.getUsername())
     22    public String generateToken(User user) {
     23        return Jwts.builder().setSubject(user.getUsername())
     24                .claim("name", user.getName())
     25                .claim("role", user.getRole())
     26                .claim("access", user.isHasAccess())
    2327                .setIssuedAt(new Date())
    2428                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24))
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/intef/JwtService.java

    r28b3398 rb248810  
    11package mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef;
    22
     3import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.User;
    34import org.springframework.security.core.userdetails.UserDetails;
    45
     
    67
    78public interface JwtService {
    8     String generateToken(UserDetails userDetails);
     9    String generateToken(User user);
    910    String generateRefreshToken(Map<String, Object> extraClaims, UserDetails userDetails);
    1011    String extractUsername(String token);
  • jobvista-frontend/package-lock.json

    r28b3398 rb248810  
    1919        "axios": "^1.6.8",
    2020        "formik": "^2.4.6",
     21        "jwt-decode": "^4.0.0",
    2122        "primereact": "^10.6.6",
    2223        "quill": "^2.0.2",
     
    1296312964      }
    1296412965    },
     12966    "node_modules/jwt-decode": {
     12967      "version": "4.0.0",
     12968      "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
     12969      "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
     12970      "engines": {
     12971        "node": ">=18"
     12972      }
     12973    },
    1296512974    "node_modules/keyv": {
    1296612975      "version": "4.5.4",
  • jobvista-frontend/package.json

    r28b3398 rb248810  
    1414    "axios": "^1.6.8",
    1515    "formik": "^2.4.6",
     16    "jwt-decode": "^4.0.0",
    1617    "primereact": "^10.6.6",
    1718    "quill": "^2.0.2",
  • jobvista-frontend/src/App.js

    r28b3398 rb248810  
    11import logo from './logo.svg';
    22import './App.css';
    3 import {useDispatch} from "react-redux";
     3import {useDispatch, useSelector} from "react-redux";
    44import {BrowserRouter} from "react-router-dom";
    55import {Header} from "./views/static/Header";
    66import {RoutesConfig} from "./auth/RoutesConfig";
    7 import {useEffect} from "react";
     7import {useEffect, useState} from "react";
    88import {AuthActions} from "./redux/actions/authActions";
    99import {AUTH_TOKEN} from "./axios/axiosInstance";
     10import {jwtDecode} from "jwt-decode";
     11import {NoAccess} from "./views/static/NoAccess";
    1012
    1113function App() {
     
    1618    }, [dispatch])
    1719
     20    const [user, setUser] = useState(null);
     21    const [loading, setLoading] = useState(true);
     22    const auth = useSelector(state => state.auth);
     23
     24    useEffect(() => {
     25        const token = localStorage.getItem(AUTH_TOKEN);
     26        if (token !== null) {
     27            try {
     28                const decodedToken = jwtDecode(token);
     29                setUser({
     30                    name: decodedToken.name,
     31                    role: decodedToken.role,
     32                    hasAccess: auth.currentUser.access,
     33                });
     34                setLoading(false);
     35            } catch (error) {
     36                console.error('Failed to decode token', error);
     37                setLoading(false);
     38            }
     39        } else {
     40            setLoading(false);
     41        }
     42    }, [auth]);
     43
     44    if (loading) {
     45        return <NoAccess />; // Replace LoadingSpinner with your loading indicator component
     46    }
     47
    1848  return (
    1949      <div className="App">
    2050          <BrowserRouter>
    21               <Header/>
    22               <RoutesConfig/>
     51              {user === null ? (
     52                  <>
     53                      <Header />
     54                      <RoutesConfig />
     55                  </>
     56              ) : user.hasAccess ? (
     57                  <>
     58                      <Header />
     59                      <RoutesConfig />
     60                  </>
     61              ) : (
     62                  <NoAccess user={user}/>
     63              )}
     64
     65
    2366          </BrowserRouter>
    2467      </div>
  • jobvista-frontend/src/auth/RoutesConfig.js

    r28b3398 rb248810  
    99import {ApplicationsByJobAd} from "../views/applications/ApplicationsByJobAd";
    1010import {ApplicationsByJobSeeker} from "../views/applications/ApplicationsByJobSeeker";
     11import {useEffect, useState} from "react";
     12import {AUTH_TOKEN} from "../axios/axiosInstance";
     13import {jwtDecode} from "jwt-decode";
     14import {useSelector} from "react-redux";
     15import {AdminPanel} from "../views/admin_panel/AdminPanel";
    1116export const RoutesConfig = () => {
    1217
     
    2126                <Route path="/job-advertisements/:id" element={<JobAdDetails/>}></Route>
    2227                <Route path="/my-job-advertisements/:advertisement_id/applications" element={<ApplicationsByJobAd/>}></Route>
     28                <Route path="/admin-panel" element={<AdminPanel/>}></Route>
    2329            </Routes>
    2430    )
  • jobvista-frontend/src/redux/actionTypes.js

    r28b3398 rb248810  
    1818export const DOWNLOAD_RESUME = "DOWNLOAD_RESUME"
    1919
     20export const FETCH_RECRUITERS = "FETCH_RECRUITERS"
     21export const CHANGE_ACCESS = "CHANGE_ACCESS"
    2022
     23
  • jobvista-frontend/src/redux/actions/authActions.js

    r28b3398 rb248810  
    4848                    email: response.email,
    4949                    name: response.name,
    50                     role: response.role
     50                    role: response.role,
     51                    access: response.hasAccess,
    5152                };
    5253                dispatch({
  • jobvista-frontend/src/redux/reducers/jobAdvertisementReducer.js

    r28b3398 rb248810  
    55    FETCH_JOB_ADVERTISEMENTS_BY_RECRUITER, FILTER_JOB_ADVERTISEMENTS, FILTER_JOB_ADVERTISEMENTS_BY_RECRUITER
    66} from "../actionTypes";
    7 import {sortElementsByDateCreated} from "../../utils/utils";
     7import {sortElementsBy} from "../../utils/utils";
    88import {useSelector} from "react-redux";
    99
     
    2222            return {
    2323                ...state,
    24                 jobAdvertisements: sortElementsByDateCreated([...state.jobAdvertisements, action.jobAdvertisement]),
    25                 jobAdvertisementsByRecruiter: sortElementsByDateCreated([...state.jobAdvertisementsByRecruiter, action.jobAdvertisement])
     24                jobAdvertisements: sortElementsBy([...state.jobAdvertisements, action.jobAdvertisement]),
     25                jobAdvertisementsByRecruiter: sortElementsBy([...state.jobAdvertisementsByRecruiter, action.jobAdvertisement], "postedOn")
    2626            }
    2727        case EDIT_JOB_ADVERTISEMENT:
     
    3030
    3131            return {
    32                 jobAdvertisements: sortElementsByDateCreated([...jobAdvertisements, action.jobAdvertisement]),
    33                 jobAdvertisementsByRecruiter: sortElementsByDateCreated([...jobAdvertisementsByRecruiter, action.jobAdvertisement])
     32                jobAdvertisements: sortElementsBy([...jobAdvertisements, action.jobAdvertisement], "postedOn"),
     33                jobAdvertisementsByRecruiter: sortElementsBy([...jobAdvertisementsByRecruiter, action.jobAdvertisement], "postedOn")
    3434            }
    3535        case DELETE_JOB_ADVERTISEMENT:
     
    3838
    3939            return {
    40                 jobAdvertisements: sortElementsByDateCreated([...jobAdvertisements]),
    41                 jobAdvertisementsByRecruiter: sortElementsByDateCreated([...jobAdvertisementsByRecruiter])
     40                jobAdvertisements: sortElementsBy([...jobAdvertisements], "postedOn"),
     41                jobAdvertisementsByRecruiter: sortElementsBy([...jobAdvertisementsByRecruiter], "postedOn")
    4242            }
    4343
     
    4545            return {
    4646                ...state,
    47                 jobAdvertisements: sortElementsByDateCreated(action.jobAdvertisements)
     47                jobAdvertisements: sortElementsBy(action.jobAdvertisements, "postedOn")
    4848            }
    4949
     
    5252            return {
    5353                ...state,
    54                 jobAdvertisementsByRecruiter: sortElementsByDateCreated(action.jobAdvertisementsByRecruiter)
     54                jobAdvertisementsByRecruiter: sortElementsBy(action.jobAdvertisementsByRecruiter, "postedOn")
    5555            }
    5656
  • jobvista-frontend/src/redux/store.js

    r28b3398 rb248810  
    44import jobAdReducer from "./reducers/jobAdvertisementReducer";
    55import applicationReducer from "./reducers/applicationReducer"
     6import adminReducer from "./reducers/adminReducer"
     7import {AdminActions} from "./actions/adminActions";
    68
    79// const rootReducer = combineReducers({
     
    1820        auth: authReducer,
    1921        jobAd: jobAdReducer,
    20         appl: applicationReducer
     22        appl: applicationReducer,
     23        admin: adminReducer
    2124    },
    2225});
  • jobvista-frontend/src/utils/utils.js

    r28b3398 rb248810  
    11
    22
    3 export const sortElementsByDateCreated = (array) => {
     3export const sortElementsBy = (array, column) => {
    44    return array.slice().sort((a, b) => {
    5         return new Date(b.postedOn).getTime() - new Date(a.postedOn).getTime()
     5        return new Date(b[column]).getTime() - new Date(a[column]).getTime()
    66    });
    77}
     
    99export const sortElementsBySubmissionDate = (array) => {
    1010    return array.slice().sort((a, b) => {
    11         return new Date(b.postedOn).getTime() - new Date(a.postedOn).getTime()
     11        return new Date(b).getTime() - new Date(a.postedOn).getTime()
    1212    });
    1313}
  • jobvista-frontend/src/views/dashboard/Dashboard.js

    r28b3398 rb248810  
    44import {useEffect, useState} from "react";
    55import {JobAdvertisementActions} from "../../redux/actions/jobAdvertisementActions";
    6 import {formatRelativeTime, sortElementsByDateCreated} from "../../utils/utils";
     6import {formatRelativeTime, sortElementsBy} from "../../utils/utils";
    77import {dataRangeOptions, industryOptions, industryOptionsFilter, sortOptions} from "../selectOptions";
    88import Select from "react-select";
     
    1010import {Link} from "react-router-dom";
    1111import JobType from "../../enumerations/JobType";
     12import {AUTH_TOKEN} from "../../axios/axiosInstance";
     13import {jwtDecode} from "jwt-decode";
    1214
    13 export const Dashboard = () => {
     15export const Dashboard = (props) => {
    1416
    1517    const dispatch = useDispatch();
     
    1719    const [jobAdvertisements, setJobAdvertisements] = useState([]);
    1820    let jobAdvertisementsState = useSelector(state => state.jobAd.jobAdvertisements)
    19     const auth = useSelector(state => state.auth.currentUser);
     21    const auth = useSelector(state => state.auth);
    2022
    21     const [role, setRole] = useState("");
     23    // const [role, setRole] = useState("");
    2224    const [selectedSortOrder, setSelectedSortOrder] = useState("date_newest");
    2325    const [selectedIndustry, setSelectedIndustry] = useState("all");
     
    2527    const [dispatched, setDispatched] = useState(false)
    2628
    27     useEffect(() => {
    28         if (auth) {
    29             setRole(auth.role);
    30         }
    31     }, [auth]);
     29    // const [user, setUser] = useState(null);
     30    //
     31    // useEffect(() => {
     32    //     const token = localStorage.getItem(AUTH_TOKEN);
     33    //     if (token!=null) {
     34    //         try {
     35    //             const decodedToken = jwtDecode(token);
     36    //             setUser({
     37    //                 name: decodedToken.name,
     38    //                 role: decodedToken.role,
     39    //                 hasAccess: decodedToken.access,
     40    //             });
     41    //         } catch (error) {
     42    //             console.error('Failed to decode token', error);
     43    //         }
     44    //     }
     45    //     console.log(user)
     46    // }, [auth]);
     47
     48    // useEffect(() => {
     49    //     if (auth) {
     50    //         setRole(auth.role);
     51    //     }
     52    //     console.log(props)
     53    // }, [auth]);
    3254
    3355    useEffect(() => {
     
    3557            dispatch(JobAdvertisementActions.fetchJobAdvertisements((success, response) => {
    3658                if (success && response.data.length > 0) {
    37                     setJobAdvertisements(sortElementsByDateCreated(response.data))
     59                    setJobAdvertisements(sortElementsBy(response.data))
    3860                }
    3961                setDispatched(true)
     
    6284
    6385    return (
     86
    6487        <div className="container">
    6588            <div className="head-dashboard-box">
  • jobvista-frontend/src/views/job_advertisements/JobAdvertisements.js

    r28b3398 rb248810  
    55import {useEffect, useState} from "react";
    66import {JobAdvertisementActions} from "../../redux/actions/jobAdvertisementActions";
    7 import {formatRelativeTime, sortElementsByDateCreated} from "../../utils/utils";
     7import {formatRelativeTime, sortElementsBy} from "../../utils/utils";
    88import {dataRangeOptions, industryOptions, industryOptionsFilter, sortOptions} from "../selectOptions";
    99import Select from "react-select";
     
    3737            dispatch(JobAdvertisementActions.fetchJobAdvertisementsByRecruiter((success, response) => {
    3838                if (success && response.data.length > 0) {
    39                     setJobAdvertisementsByRecruiter(sortElementsByDateCreated(response.data))
     39                    setJobAdvertisementsByRecruiter(sortElementsBy(response.data))
    4040                }
    4141                console.log("Fetch job advertisements by recruiter GET")
  • jobvista-frontend/src/views/static/Header.js

    r28b3398 rb248810  
    11import {Link, NavLink} from "react-router-dom";
    22import "./Header.css"
     3import { jwtDecode } from "jwt-decode";
    34import {useDispatch, useSelector} from 'react-redux';
    45import {useEffect, useState} from "react";
     
    67import Roles from "../../enumerations/Roles";
    78import {useNavigate} from "react-router";
     9import {AUTH_TOKEN} from "../../axios/axiosInstance";
    810
    911export const Header = (props) => {
     
    1618    const [username, setUsername] = useState("");
    1719
     20    const [user, setUser] = useState("");
     21
    1822    const signOut = () => {
    1923        dispatch(AuthActions.signOut());
    2024        window.location = "/";
    2125    }
     26
     27    useEffect(() => {
     28        const token = localStorage.getItem(AUTH_TOKEN);
     29        if (token!=null) {
     30            try {
     31                const decodedToken = jwtDecode(token);
     32                setUser({
     33                    name: decodedToken.name,
     34                    role: decodedToken.role,
     35                    hasAccess: decodedToken.hasAccess,
     36                });
     37            } catch (error) {
     38                console.error('Failed to decode token', error);
     39            }
     40        }
     41    }, [auth]);
    2242
    2343    useEffect(() => {
     
    4969                            </>
    5070                        }
     71                        {role===Roles.ADMIN &&
     72                            <>
     73                                <NavLink to="/admin-panel" className="nav-item nav-link">Admin Panel</NavLink>
     74                            </>
     75
     76                        }
    5177                        <NavLink to="/about" className="nav-item nav-link">About</NavLink>
    5278                        <NavLink to="/contact" className="nav-item nav-link">Support</NavLink>
     
    5783                            <img src="/images/user.png" width="45" height="45"/>
    5884                            <div className="auth-box">
    59                                 <p className="user"><b>{username}</b></p>
    60                                 <p className="role">{role==Roles.RECRUITER ? "Recruiter" : "Job Seeker"}</p>
     85                                <p className="user"><b>{user.name}</b></p>
     86                                {user.role==Roles.RECRUITER && <p className="role">Recruiter</p>}
     87                                {user.role==Roles.JOBSEEKER && <p className="role">Job Seeker</p>}
     88                                {user.role==Roles.ADMIN && <p className="role">Admin</p>}
     89                                {/*<p className="role">{user.role==Roles.RECRUITER ? "Recruiter" : "Job Seeker"}</p>*/}
    6190                            </div>
    6291
Note: See TracChangeset for help on using the changeset viewer.