diff --git a/client/package-lock.json b/client/package-lock.json
index eec1c19..82bf3b1 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -6400,6 +6400,16 @@
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
},
+ "highcharts": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-8.2.2.tgz",
+ "integrity": "sha512-F63TXO7RxsvTcpO/KOubQZWualYpCMyCTuKtoWbt7KCsfQ3Kl7Fr6HEyyJdjkYl+XlnmnKlSRi9d3HjLK9Q0wg=="
+ },
+ "highcharts-react-official": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/highcharts-react-official/-/highcharts-react-official-3.0.0.tgz",
+ "integrity": "sha512-VefJgDY2hkT9gfppsQGrRF2g5u8d9dtfHGcx2/xqiP+PkZXCqalw9xOeKVCRvJKTOh0coiDFwvVjOvB7KaGl4A=="
+ },
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
diff --git a/client/package.json b/client/package.json
index 02e00fb..23d6297 100644
--- a/client/package.json
+++ b/client/package.json
@@ -12,6 +12,8 @@
"@testing-library/user-event": "^7.1.2",
"axios": "^0.20.0",
"graphql": "^15.3.0",
+ "highcharts": "^8.2.2",
+ "highcharts-react-official": "^3.0.0",
"jwt-decode": "^3.0.0",
"prop-types": "^15.7.2",
"react": "^16.13.1",
diff --git a/client/src/App.js b/client/src/App.js
index 312b71c..6ee7545 100644
--- a/client/src/App.js
+++ b/client/src/App.js
@@ -12,6 +12,7 @@ import { fetchIsDrawerOpen } from './redux/slices/drawer';
import { fetchLogin } from './redux/slices/auth';
import './App.css';
+import Dashboard from './components/dashboard';
const useStyles = makeStyles((theme) => ({
root: {
@@ -39,12 +40,12 @@ function App() {
const dispatch = useDispatch();
const snackbarState = useSelector((state) => state.snackbar);
-
+ const token = useSelector((state) => state.auth.authToken);
// Fetch the initial drawer open state
useEffect(() => {
dispatch(fetchIsDrawerOpen());
dispatch(fetchLogin());
- }, [dispatch]);
+ }, [dispatch, token]);
return (
@@ -62,6 +63,9 @@ function App() {
+
+
+
{/* Auth related routes */}
diff --git a/client/src/components/auth/Login.js b/client/src/components/auth/Login.js
index 3343c0e..9ac844d 100644
--- a/client/src/components/auth/Login.js
+++ b/client/src/components/auth/Login.js
@@ -20,6 +20,7 @@ import axios from '../../utils/axios';
import loginQuery from '../../graphQl/queries/loginQuery';
import { setIsSnackbarOpen } from '../../redux/slices/snackbar';
import { setLogin } from '../../redux/slices/auth';
+import { fetchUrls } from '../../redux/slices/urls';
// Define styles for this component
const useStyles = makeStyles((theme) => ({
@@ -108,6 +109,7 @@ function Login() {
// Get the jwt and store in redux and local storage
const authToken = response.data.data.login.token;
dispatch(setLogin({ isLoggedIn: true, authToken }));
+ dispatch(fetchUrls({ token: authToken }));
history.replace(prevPath, location.state);
} catch (err) {
diff --git a/client/src/components/dashboard/Graph/LineChart.js b/client/src/components/dashboard/Graph/LineChart.js
new file mode 100644
index 0000000..b21dbf4
--- /dev/null
+++ b/client/src/components/dashboard/Graph/LineChart.js
@@ -0,0 +1,83 @@
+import React from 'react';
+import Highcharts from 'highcharts/highstock';
+import HighchartsReact from 'highcharts-react-official';
+import { useSelector } from 'react-redux';
+
+/**
+ * Line Chart component
+ * This renders a line chart plotting the no of clicks made per day
+ */
+
+// The component to render the line chart
+function LineChart() {
+ // Retreive the selected Url
+ const selectedUrl = useSelector((state) => state.urls.selectedUrl);
+ const clicks = selectedUrl ? selectedUrl.clicks : [];
+
+ // Group the Clicks according to date
+ const groups = clicks.reduce((groups, click) => {
+ // Retrieve the Date from clicked time
+ const date = click.time.split('T')[0];
+ if (!groups[date]) {
+ groups[date] = [];
+ }
+ groups[date].push(click);
+ return groups;
+ }, {});
+ // Generate an array according to the groups made
+ const groupArrays = Object.keys(groups).map((date) => {
+ return {
+ date,
+ clicks: groups[date],
+ };
+ });
+
+ // Data to be passed to the chart
+ const options = {
+ chart: {
+ reflow: 'true',
+ BackgroundColor: '#fbfbfb',
+ plotBorderWidth: null,
+ plotShadow: false,
+ type: 'line',
+ },
+ title: {
+ text: 'Clicks by Date ',
+ align: 'left',
+ },
+ xAxis: {
+ title: {
+ text: 'Date',
+ },
+ categories: groupArrays.map((groups) => groups.date),
+ },
+ yAxis: {
+ title: {
+ text: 'Clicks',
+ },
+ },
+ series: [{
+ name: 'Clicks',
+ data: groupArrays.map((groups) => groups.clicks.length),
+ }],
+ };
+
+ return (
+ );
+}
+
+export default LineChart;
diff --git a/client/src/components/dashboard/Graph/PieChart.js b/client/src/components/dashboard/Graph/PieChart.js
new file mode 100644
index 0000000..7d5c2f3
--- /dev/null
+++ b/client/src/components/dashboard/Graph/PieChart.js
@@ -0,0 +1,157 @@
+import HighchartsReact from 'highcharts-react-official';
+import Highcharts from 'highcharts/highstock';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { useSelector } from 'react-redux';
+
+/**
+ * This is the Pie Chart component
+ * This has two variants
+ * 1. This plots the no of clicks made in the last month per location on a pieChart
+ * 2. This plots the no of clicks made in the last month per url on a pieChart
+ * @usage Just import it and pass a variant prop
+ * whose value should be `CLICKS_BY_LOCATION` for 1 else `CLICKS_BY_URLS` for 2
+ * @example
+ */
+
+export default function PieChart(props) {
+ const { variant } = props;
+
+ const urls = useSelector((state) => state.urls.urls);
+ const selectedUrl = useSelector((state) => state.urls.selectedUrl);
+ const clicks = selectedUrl ? selectedUrl.clicks : [];
+ let options = null;
+
+ // Check if the variant is one needing location
+ if (variant === 'CLICKS_BY_LOCATION') {
+ const groups = clicks.reduce((groups, click) => {
+ const location = click.location.city + ',' + click.location.state;
+ if (!groups[location]) {
+ groups[location] = [];
+ }
+ const date1 = new Date();
+ const date2 = new Date(click.time.split('T')[0]);
+ const diffDays = Math.ceil((date1 - date2) / (1000 * 60 * 60 * 24));
+ if (diffDays <= 31) {
+ groups[location].push(click);
+ }
+ return groups;
+ }, {});
+ const groupArrays = Object.keys(groups).map((location) => {
+ return {
+ name: location,
+ y: groups[location].length,
+ };
+ });
+ options = {
+ chart: {
+ plotBackgroundColor: '#fbfbfb',
+ plotBorderWidth: null,
+ plotShadow: false,
+ type: 'pie',
+ },
+ legend: {
+ layout: 'vertical',
+ align: 'right',
+ verticalAlign: 'middle',
+ },
+ title: {
+ text: 'Clicks by Location',
+ align: 'left',
+ },
+ tooltip: {
+ pointFormat: '{series.name}: {point.percentage:.1f}%',
+ },
+ plotOptions: {
+ pie: {
+ dataLabels: {
+ enabled: true,
+ format: '{point.name}:{point.percentage:.1f} %',
+ },
+ },
+ },
+ series: [{
+ name: 'Clicks',
+ colorByPoint: true,
+ data: groupArrays,
+ }],
+ };
+ } else if (variant === 'CLICKS_BY_URLS') {
+ const groups = urls.reduce((groups, url) => {
+ const clicks = url.clicks ? url.clicks : [];
+ const title = url.title ? url.title : url.shortUrl;
+ for (const click of clicks) {
+ const date1 = new Date();
+ const date2 = new Date(click.time.split('T')[0]);
+ const diffDays = Math.ceil((date1 - date2) / (1000 * 60 * 60 * 24));
+ if (!groups[title]) {
+ groups[title] = [];
+ }
+ if (diffDays <= 31) {
+ groups[title].push(click);
+ }
+ }
+ return groups;
+ }, {});
+ const groupArrays = Object.keys(groups).map((title) => {
+ return {
+ name: title,
+ y: groups[title].length,
+ };
+ });
+ options = {
+ chart: {
+ plotBackgroundColor: '#fbfbfb',
+ plotBorderWidth: null,
+ plotShadow: false,
+ type: 'pie',
+ },
+ legend: {
+ layout: 'vertical',
+ align: 'right',
+ verticalAlign: 'middle',
+ },
+ title: {
+ text: 'Clicks last month',
+ align: 'left',
+ },
+ tooltip: {
+ pointFormat: '{series.name}: {point.percentage:.1f}%',
+ },
+ plotOptions: {
+ pie: {
+ dataLabels: {
+ enabled: true,
+ format: '{point.name}:{point.percentage:.1f} %',
+ },
+ },
+ },
+ series: [{
+ name: 'Clicks',
+ colorByPoint: true,
+ data: groupArrays,
+ }],
+ };
+ }
+ return (
+
+ );
+}
+
+PieChart.propTypes = {
+ variant: PropTypes.string,
+};
diff --git a/client/src/components/dashboard/RecentUrlcontainer.js b/client/src/components/dashboard/RecentUrlcontainer.js
new file mode 100644
index 0000000..300513b
--- /dev/null
+++ b/client/src/components/dashboard/RecentUrlcontainer.js
@@ -0,0 +1,61 @@
+import {
+ Card,
+ CardActions,
+ CardHeader,
+ makeStyles,
+ useMediaQuery,
+ useTheme,
+} from '@material-ui/core';
+import React from 'react';
+import UrlList from './UrlList';
+
+// Create styles for the component
+const useStyles = makeStyles((theme) => ({
+ container: {
+ display: 'flex',
+ flexGrow: 1,
+ flexBasis: '50%',
+ },
+}));
+
+// Url Container component
+const UrlContainer = (props) => {
+ const theme = useTheme();
+ const classes = useStyles();
+ const mediaMinSm = useMediaQuery(theme.breakpoints.up('sm'));
+
+ return (
+ );
+};
+
+export default UrlContainer;
diff --git a/client/src/components/dashboard/UrlList/index.css b/client/src/components/dashboard/UrlList/index.css
new file mode 100644
index 0000000..ad2de61
--- /dev/null
+++ b/client/src/components/dashboard/UrlList/index.css
@@ -0,0 +1,10 @@
+.item{
+ cursor: 'pointer';
+ height: '12%';
+}
+.active{
+ display: flex;
+ width: '100%';
+ background-color: slategray;
+ color: white;
+}
\ No newline at end of file
diff --git a/client/src/components/dashboard/UrlList/index.js b/client/src/components/dashboard/UrlList/index.js
new file mode 100644
index 0000000..0aeecf8
--- /dev/null
+++ b/client/src/components/dashboard/UrlList/index.js
@@ -0,0 +1,157 @@
+import { makeStyles } from '@material-ui/core';
+import PropTypes from 'prop-types';
+import React, { useCallback, useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { setIsSnackbarOpen } from '../../../redux/slices/snackbar';
+import { fetchUrls, setSelectedUrl } from '../../../redux/slices/urls';
+import './index.css';
+
+// Create the styles for the components
+const useStyles = makeStyles((theme) => ({
+ list: {
+ display: 'flex',
+ flexDirection: 'column',
+ width: '100%',
+ height: '100%',
+ },
+}));
+
+// Function to style and render an url item
+const UrlItem = ({ url, onClick, active }) => {
+ // Extract and store the date of creation
+ const createdAt = new Date(url.createdAt).toDateString().substring(4);
+ /** Check if current url has been clicked or not
+ * if yes then then extract its length
+ * else store 0
+ */
+ const clicksLength = url.clicks ? url.clicks.length : 0;
+
+ return (
+
+ );
+};
+
+const UrlList = (props) => {
+ const [selected, setSelected] = useState();
+ const classes = useStyles();
+ const dispatch = useDispatch();
+ const token = useSelector((state) => state.auth.authToken);
+ const urls = useSelector((state) => state.urls.urls);
+ const error = useSelector((state) => state.urls.error);
+
+ /** Function to dispatch the fetch Urls reducer
+ * which updates the url list current component is using if neccessary
+ */
+ const fetchingUrls = useCallback(() => {
+ // fetch the urls only if user is logged in
+ if (token) {
+ dispatch(fetchUrls({ token }));
+ }
+ }, [dispatch, token]);
+ // Slice out the five most recent Urls
+
+ useEffect(() => {
+ fetchingUrls();
+ },
+ [fetchingUrls]);
+
+ // If there is some error in fetching urls then notify the user accordingly
+ if (error) {
+ dispatch(setIsSnackbarOpen({
+ status: 'open',
+ message: 'Some Unknown Error occured in fetching urls, Please Refresh',
+ severity: 'error',
+ }));
+ }
+ return (
+
+
+
+
+ {urls.map((url, index) => {
+ return {
+ dispatch(setSelectedUrl({ selectedUrl: url }));
+ return setSelected(index);
+ }}/>;
+ })}
+
+
+ );
+};
+
+UrlItem.propTypes = {
+ url: PropTypes.object,
+ onClick: PropTypes.func,
+ active: PropTypes.bool,
+};
+
+export default UrlList;
diff --git a/client/src/components/dashboard/index.js b/client/src/components/dashboard/index.js
new file mode 100644
index 0000000..1353ba5
--- /dev/null
+++ b/client/src/components/dashboard/index.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import {
+ CssBaseline,
+ makeStyles,
+} from '@material-ui/core';
+import UrlContainer from './RecentUrlcontainer';
+import PieChart from './Graph/PieChart';
+import LineChart from './Graph/LineChart';
+
+// Create styles for this component
+const useStyles = makeStyles((theme) => ({
+ root: {
+ margin: '10px',
+ },
+ container: {
+ 'display': 'flex',
+ 'height': '100%',
+ 'width': '100%',
+ },
+ leftContainer: {
+ display: 'flex',
+ flexDirection: 'column',
+ margin: '10px',
+ flexGrow: 3,
+ },
+ rightContainer: {
+ display: 'flex',
+ flexDirection: 'column',
+ padding: '10px',
+ alignItems: 'center',
+ flexGrow: 2,
+ },
+}));
+
+// Function to render the Dashboard component
+export default function Dashboard() {
+ const classes = useStyles();
+ return (
+
+ );
+};
diff --git a/client/src/graphQl/queries/getUrlsQuery.js b/client/src/graphQl/queries/getUrlsQuery.js
new file mode 100644
index 0000000..ab08fe3
--- /dev/null
+++ b/client/src/graphQl/queries/getUrlsQuery.js
@@ -0,0 +1,27 @@
+
+const getUrlsQuery = () => {
+ const graphqlQuery = {
+ query: `query{
+ getUrls{
+ urls{
+ owner
+ _id
+ title
+ longUrl
+ shortUrl
+ createdAt
+ updatedAt
+ clicks{
+ ip
+ time
+ location{
+ city
+ state
+ }
+ }
+ }
+ }
+}` };
+ return graphqlQuery;
+};
+export default getUrlsQuery;
diff --git a/client/src/graphQl/queries/loginQuery.js b/client/src/graphQl/queries/loginQuery.js
index 2a39ac4..b9fa0a2 100644
--- a/client/src/graphQl/queries/loginQuery.js
+++ b/client/src/graphQl/queries/loginQuery.js
@@ -1,4 +1,4 @@
-const loginQuery=(authData)=>{
+const loginQuery = (authData) => {
const graphqlQuery = {
query: `
query{
diff --git a/client/src/redux/actionTypes.js b/client/src/redux/actionTypes.js
index 695696f..d98e987 100644
--- a/client/src/redux/actionTypes.js
+++ b/client/src/redux/actionTypes.js
@@ -2,3 +2,5 @@ export const SET_IS_DRAWER_OPEN = 'SET_IS_DRAWER_OPEN';
export const FETCH_IS_DRAWER_OPEN = 'FETCH_IS_DRAWER_OPEN';
export const LOGIN = 'LOGIN';
export const FETCH_LOGIN = 'FETCH_LOGIN';
+export const FETCH_URLS = 'FETCH_URLS';
+export const SET_SELECTED_URL = 'SET_SELECTED_URL';
diff --git a/client/src/redux/rootReducer.js b/client/src/redux/rootReducer.js
index 26bc3b5..1bc22b3 100644
--- a/client/src/redux/rootReducer.js
+++ b/client/src/redux/rootReducer.js
@@ -2,9 +2,10 @@ import { combineReducers } from 'redux';
import { drawerSlice } from './slices/drawer';
import { authSlice } from './slices/auth';
import { snackbarSlice } from './slices/snackbar';
-
+import { urlSlice } from './slices/urls';
export default combineReducers({
[drawerSlice.name]: drawerSlice.reducer,
[authSlice.name]: authSlice.reducer,
[snackbarSlice.name]: snackbarSlice.reducer,
+ [urlSlice.name]: urlSlice.reducer,
});
diff --git a/client/src/redux/slices/urls.js b/client/src/redux/slices/urls.js
new file mode 100644
index 0000000..9a23b0c
--- /dev/null
+++ b/client/src/redux/slices/urls.js
@@ -0,0 +1,47 @@
+import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
+import getUrlsQuery from '../../graphQl/queries/getUrlsQuery';
+import axios from '../../utils/axios';
+
+import { FETCH_URLS, SET_SELECTED_URL } from '../actionTypes';
+
+export const fetchUrls = createAsyncThunk(FETCH_URLS,
+ async (payload) => {
+ try {
+ const { token } = payload;
+ const graphqlQuery = getUrlsQuery();
+ const response = await axios.post('/', graphqlQuery, {
+ headers: {
+ Authorization: token,
+ },
+ });
+ const urls = response.data.data.getUrls.urls;
+
+ return { urls };
+ } catch (err) {
+ return { error: true };
+ }
+ });
+
+export const setSelectedUrl = createAsyncThunk(SET_SELECTED_URL,
+ async (payload) => {
+ const { selectedUrl } = payload;
+ localStorage.setItem('selectedUrlId', selectedUrl._id);
+ return {
+ selectedUrl,
+ };
+ });
+
+export const urlSlice = createSlice({
+ name: 'urls',
+ initialState: { urls: [], selectedUrl: null, error: false },
+ extraReducers: {
+ [fetchUrls.fulfilled]: (state, action) => ({
+ ...state,
+ ...action.payload,
+ }),
+ [setSelectedUrl.fulfilled]: (state, action) => ({
+ ...state,
+ ...action.payload,
+ }),
+ },
+});
diff --git a/package.json b/package.json
index 5698e01..49bbb25 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"install": "concurrently --kill-others-on-fail \"cd ./client && npm install\" \"cd ./server && npm install\" \"cd ./redirection-server && npm install\"",
"start-client": "cd ./client && npm start",
"start-server": "cd ./server && npm start",
- "start-redirection-server":"cd ./redirection-server && npm start",
+ "start-redirection-server": "cd ./redirection-server && npm start",
"dev": "concurrently --kill-others-on-fail \"npm run start-server\" \"npm run start-client\" \"npm run start-redirection-server\"",
"start": "npm run start-server",
"test": "echo \"Error: no test specified\" && exit 1"
diff --git a/redirection-server/middlewares/redirect.js b/redirection-server/middlewares/redirect.js
index 0e53c16..4d5132e 100644
--- a/redirection-server/middlewares/redirect.js
+++ b/redirection-server/middlewares/redirect.js
@@ -38,19 +38,21 @@ const redirect = async (req, res) => {
* after every redirection request
*/
- const ip = (req.headers['x-forwarded-for'] || '').split(',').pop().trim() ||
+ let ip = (req.headers['x-forwarded-for'] || '').split(',').pop().trim() ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
req.connection.socket.remoteAddress;
+ if (ip === '::1') {
+ ip = '49.37.5.225';
+ }
const time = new Date().toISOString();
const apiEndpoint = BASE_URL + ip + '?access_key=' + key;
const location = await axios.get(apiEndpoint);
const click = new Click({
- location: location.data,
+ location: { city: location.data.city, state: location.data.region_name },
time,
ip,
});
- await click.save();
return Url.updateOne({ shortUrl: hash }, {
$push: {
clicks: click,
diff --git a/redirection-server/models/Url.js b/redirection-server/models/Url.js
index 91996ca..23c0f2c 100644
--- a/redirection-server/models/Url.js
+++ b/redirection-server/models/Url.js
@@ -12,9 +12,25 @@ const urlSchema = new Schema(
type: String,
required: true,
},
+ title: {
+ type: String,
+ },
+ description: {
+ type: String,
+ },
clicks: [{
- type: mongoose.Types.ObjectId,
- ref: 'Click',
+ location: {
+ type: 'Object',
+ required: true,
+ },
+ time: {
+ type: String,
+ required: true,
+ },
+ ip: {
+ type: String,
+ required: true,
+ },
}],
owner: {
type: mongoose.Types.ObjectId,
diff --git a/redirection-server/package-lock.json b/redirection-server/package-lock.json
index 7e039ac..3886437 100644
--- a/redirection-server/package-lock.json
+++ b/redirection-server/package-lock.json
@@ -1015,8 +1015,7 @@
"follow-redirects": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz",
- "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==",
- "dev": true
+ "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA=="
},
"forwarded": {
"version": "0.1.2",
diff --git a/server/graphQl/resolvers.js b/server/graphQl/resolvers.js
index c0df23e..9c2e6b0 100644
--- a/server/graphQl/resolvers.js
+++ b/server/graphQl/resolvers.js
@@ -4,6 +4,12 @@ const urlShortener = require('../utils/urlShortener');
const validator = require('../validator');
const User = require('../models/User');
const Url = require('../models/Url');
+const { GraphQLDateTime } = require('graphql-iso-date');
+const { ObjectID } = require('mongodb');
+
+const customScalarResolver = {
+ Date: GraphQLDateTime,
+};
// Sign in resolver
const signUp = async ({ UserInput }) => {
@@ -194,6 +200,29 @@ const addDetails = async ({ title, description, shortUrl, updatedShortUrl }, req
};
}
};
+
+// Resolver to fetch all the urls belonging to the current user
+const getUrls = async ({ }, request) => {
+ // Get the user id from the request
+ const { userId } = request;
+ // If user is not logged in then ask him to login first
+ if (!userId) {
+ const error = new Error('Please Login');
+ error.code = 401;
+ throw error;
+ }
+ const urls = await Url.aggregate([{
+ $match: {
+ owner: new ObjectID(userId),
+ },
+ }, {
+ $sort: { 'createdAt': -1 },
+ }],
+ );
+ // Retrieve all the urls owned by the user
+ return { urls };
+};
+
module.exports = {
- signUp, login, shortenUrl, addDetails,
+ signUp, login, shortenUrl, addDetails, getUrls, customScalarResolver,
};
diff --git a/server/graphQl/schema.js b/server/graphQl/schema.js
index 366dd21..e686586 100644
--- a/server/graphQl/schema.js
+++ b/server/graphQl/schema.js
@@ -1,34 +1,37 @@
const { buildSchema } = require('graphql');
module.exports = buildSchema(`
+ scalar Date
type Url{
- _id : ID!
- ownerid : ID!
+ _id : String!
+ owner : String!
longUrl : String!
shortUrl : String!
- expirydate : String!
- createdAt : String!
- readwriteaccess : Access!
+ expirydate : Date!
+ title : String
+ description : String
+ createdAt : Date!
+ updatedAt : Date!
+ readaccess : [User]
+ writeaccess : [User]
clicks : [Click!]
}
type User{
- _id : ID!
+ _id : String!
name : String!
email : String!
password : String!
- createdAt : String!
- updatedAt : String!
- urls : [Url!]
+ createdAt : Date!
+ updatedAt : Date!
}
-
- type Click{
- email : String!
- clickedAt : String!
- location : String!
+ type location{
+ city : String
+ state : String
}
- type Access{
- email : String!
- permissions : [String!]!
+ type Click{
+ ip : String!
+ time : Date!
+ location : location!
}
type AuthToken{
token : String!
@@ -41,21 +44,20 @@ module.exports = buildSchema(`
email : String!
password : String!
}
- type PostData{
- urls : [Url!]!
- totalurls : Int!
- }
type RootQuery{
login(email : String! , password : String!) : AuthToken!
- urls : PostData!
+ getUrls: urls!
}
type shortUrl{
- _id : ID!,
+ _id : String!,
longUrl : String!,
shortUrl : String!,
title : String,
description : String
}
+ type urls{
+ urls : [Url!]
+ }
type RootMutation{
signUp(UserInput : UserInputData) : GenericMessage!
shortenUrl(longUrl : String!) : shortUrl!
diff --git a/server/models/Url.js b/server/models/Url.js
index 91996ca..23c0f2c 100644
--- a/server/models/Url.js
+++ b/server/models/Url.js
@@ -12,9 +12,25 @@ const urlSchema = new Schema(
type: String,
required: true,
},
+ title: {
+ type: String,
+ },
+ description: {
+ type: String,
+ },
clicks: [{
- type: mongoose.Types.ObjectId,
- ref: 'Click',
+ location: {
+ type: 'Object',
+ required: true,
+ },
+ time: {
+ type: String,
+ required: true,
+ },
+ ip: {
+ type: String,
+ required: true,
+ },
}],
owner: {
type: mongoose.Types.ObjectId,
diff --git a/server/package-lock.json b/server/package-lock.json
index cc21b20..efb8020 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -1380,6 +1380,11 @@
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz",
"integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w=="
},
+ "graphql-iso-date": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/graphql-iso-date/-/graphql-iso-date-3.6.1.tgz",
+ "integrity": "sha512-AwFGIuYMJQXOEAgRlJlFL4H1ncFM8n8XmoVDTNypNOZyQ8LFDG2ppMFlsS862BSTCDcSUfHp8PD3/uJhv7t59Q=="
+ },
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
diff --git a/server/package.json b/server/package.json
index 1a99aae..45f19a7 100644
--- a/server/package.json
+++ b/server/package.json
@@ -16,6 +16,7 @@
"express-graphql": "^0.11.0",
"express-validator": "^6.6.1",
"graphql": "^15.3.0",
+ "graphql-iso-date": "^3.6.1",
"jsonwebtoken": "^8.5.1",
"jwt-decode": "^3.0.0",
"mongodb": "^3.6.2",