Skip to content

Commit 7f7c1b8

Browse files
authored
Merge pull request #1534 from w3bdesign/login
Add login
2 parents fd440fc + d7c9b1f commit 7f7c1b8

File tree

11 files changed

+352
-9
lines changed

11 files changed

+352
-9
lines changed

.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@ NEXT_PUBLIC_PLACEHOLDER_SMALL_IMAGE_URL="https://res.cloudinary.com/placeholder-
44
NEXT_PUBLIC_PLACEHOLDER_LARGE_IMAGE_URL="https://via.placeholder.com/600"
55
NEXT_PUBLIC_ALGOLIA_APP_ID = "changeme"
66
NEXT_PUBLIC_ALGOLIA_PUBLIC_API_KEY = "changeme"
7-
NODE_ENV="development"
7+
NODE_ENV="development"
8+
NEXT_PUBLIC_AUTH_TOKEN_SS_KEY="auth-token"
9+
NEXT_PUBLIC_REFRESH_TOKEN_LS_KEY="refresh-token"
10+
NEXT_PUBLIC_SESSION_TOKEN_LS_KEY="session-token"
11+
NEXT_PUBLIC_AUTH_KEY_TIMEOUT="300000"

src/components/Header/Navbar.component.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Link from 'next/link';
2-
2+
import { useEffect, useState } from 'react';
3+
import { hasCredentials } from '../../utils/auth';
34
import Cart from './Cart.component';
45
import AlgoliaSearchBox from '../AlgoliaSearch/AlgoliaSearchBox.component';
56
import MobileSearch from '../AlgoliaSearch/MobileSearch.component';
@@ -9,6 +10,12 @@ import MobileSearch from '../AlgoliaSearch/MobileSearch.component';
910
* Includes mobile menu.
1011
*/
1112
const Navbar = () => {
13+
const [loggedIn, setLoggedIn] = useState(false);
14+
15+
useEffect(() => {
16+
setLoggedIn(hasCredentials());
17+
}, []);
18+
1219
return (
1320
<header className="border-b border-gray-200">
1421
<nav id="header" className="top-0 z-50 w-full bg-white">
@@ -51,6 +58,35 @@ const Navbar = () => {
5158
</Link>
5259
<div className="flex items-center space-x-3">
5360
<AlgoliaSearchBox />
61+
{loggedIn ? (
62+
<Link href="/min-konto">
63+
<span className="text-base uppercase tracking-wider group relative">
64+
<span className="relative inline-block">
65+
<span className="absolute -bottom-1 left-0 w-0 h-px bg-gray-900 group-hover:w-full transition-all duration-500"></span>
66+
Min konto
67+
</span>
68+
</span>
69+
</Link>
70+
) : (
71+
<>
72+
<Link href="/logg-inn">
73+
<span className="text-base uppercase tracking-wider group relative">
74+
<span className="relative inline-block">
75+
<span className="absolute -bottom-1 left-0 w-0 h-px bg-gray-900 group-hover:w-full transition-all duration-500"></span>
76+
Logg inn
77+
</span>
78+
</span>
79+
</Link>
80+
<Link href="/registrer">
81+
<span className="text-base uppercase tracking-wider group relative">
82+
<span className="relative inline-block">
83+
<span className="absolute -bottom-1 left-0 w-0 h-px bg-gray-900 group-hover:w-full transition-all duration-500"></span>
84+
Registrer
85+
</span>
86+
</span>
87+
</Link>
88+
</>
89+
)}
5490
<Cart />
5591
</div>
5692
</div>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useQuery } from '@apollo/client';
2+
import { GET_CUSTOMER_ORDERS } from '../../utils/gql/GQL_QUERIES';
3+
import LoadingSpinner from '../LoadingSpinner/LoadingSpinner.component';
4+
5+
interface Order {
6+
id: string;
7+
orderNumber: number;
8+
status: string;
9+
total: string;
10+
date: string;
11+
}
12+
13+
/**
14+
* Customer account component that displays user's orders
15+
* @function CustomerAccount
16+
* @returns {JSX.Element} - Rendered component with order history
17+
*/
18+
const CustomerAccount = () => {
19+
const { loading, error, data } = useQuery(GET_CUSTOMER_ORDERS);
20+
21+
if (loading) return <LoadingSpinner />;
22+
if (error) return <p>Error: {error.message}</p>;
23+
24+
const orders = data?.customer?.orders?.nodes;
25+
26+
return (
27+
<div>
28+
<h1 className="text-2xl font-bold mb-4">Mine ordre</h1>
29+
{orders && orders.length > 0 ? (
30+
<div className="overflow-x-auto">
31+
<table className="min-w-full bg-white">
32+
<thead>
33+
<tr>
34+
<th className="py-2 px-4 border-b">Ordrenummer</th>
35+
<th className="py-2 px-4 border-b">Dato</th>
36+
<th className="py-2 px-4 border-b">Status</th>
37+
<th className="py-2 px-4 border-b">Total</th>
38+
</tr>
39+
</thead>
40+
<tbody>
41+
{orders.map((order: Order) => (
42+
<tr key={order.id}>
43+
<td className="py-2 px-4 border-b">{order.orderNumber}</td>
44+
<td className="py-2 px-4 border-b">
45+
{new Date(order.date).toLocaleDateString()}
46+
</td>
47+
<td className="py-2 px-4 border-b">{order.status}</td>
48+
<td className="py-2 px-4 border-b">{order.total}</td>
49+
</tr>
50+
))}
51+
</tbody>
52+
</table>
53+
</div>
54+
) : (
55+
<p>Du har ingen ordre.</p>
56+
)}
57+
</div>
58+
);
59+
};
60+
61+
export default CustomerAccount;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { useState } from 'react';
2+
import { useForm, FormProvider } from 'react-hook-form';
3+
import { login } from '../../utils/auth';
4+
import { InputField } from '../Input/InputField.component';
5+
import LoadingSpinner from '../LoadingSpinner/LoadingSpinner.component';
6+
import Button from '../UI/Button.component';
7+
import { useRouter } from 'next/router';
8+
9+
interface ILoginData {
10+
username: string;
11+
password: string;
12+
}
13+
14+
/**
15+
* User login component that handles user authentication
16+
* @function UserLogin
17+
* @returns {JSX.Element} - Rendered component with login form
18+
*/
19+
const UserLogin = () => {
20+
const methods = useForm<ILoginData>();
21+
const [loading, setLoading] = useState(false);
22+
const [error, setError] = useState<string | null>(null);
23+
const router = useRouter();
24+
25+
const onSubmit = async (data: ILoginData) => {
26+
setLoading(true);
27+
setError(null);
28+
try {
29+
const result = await login(data.username, data.password);
30+
if (result.success && result.status === 'SUCCESS') {
31+
router.push('/min-konto');
32+
} else {
33+
throw new Error('Failed to login');
34+
}
35+
} catch (error: unknown) {
36+
if (error instanceof Error) {
37+
setError(error.message);
38+
} else {
39+
setError('An unknown error occurred.');
40+
}
41+
console.error('Login error:', error);
42+
} finally {
43+
setLoading(false);
44+
}
45+
};
46+
47+
return (
48+
<section className="text-gray-700 container p-4 py-2 mx-auto mb-[8rem] md:mb-0">
49+
<FormProvider {...methods}>
50+
<form onSubmit={methods.handleSubmit(onSubmit)}>
51+
<div className="mx-auto lg:w-1/2 flex flex-wrap">
52+
<InputField
53+
inputName="username"
54+
inputLabel="Brukernavn eller e-post"
55+
type="text"
56+
customValidation={{ required: true }}
57+
/>
58+
<InputField
59+
inputName="password"
60+
inputLabel="Passord"
61+
type="password"
62+
customValidation={{ required: true }}
63+
/>
64+
65+
{error && (
66+
<div className="w-full p-2 text-red-600 text-sm text-center">
67+
{error}
68+
</div>
69+
)}
70+
71+
<div className="w-full p-2">
72+
<div className="mt-4 flex justify-center">
73+
<Button variant="primary" buttonDisabled={loading}>
74+
{loading ? <LoadingSpinner /> : 'Logg inn'}
75+
</Button>
76+
</div>
77+
</div>
78+
</div>
79+
</form>
80+
</FormProvider>
81+
</section>
82+
);
83+
};
84+
85+
export default UserLogin;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useRouter } from 'next/router';
2+
import { useEffect, ComponentType } from 'react';
3+
import { hasCredentials } from '../../utils/auth';
4+
5+
const withAuth = <P extends object>(WrappedComponent: ComponentType<P>) => {
6+
const Wrapper = (props: P) => {
7+
const router = useRouter();
8+
9+
useEffect(() => {
10+
if (!hasCredentials()) {
11+
router.push('/logg-inn');
12+
}
13+
}, [router]);
14+
15+
if (!hasCredentials()) {
16+
return null; // or a loading spinner
17+
}
18+
19+
return <WrappedComponent {...props} />;
20+
};
21+
22+
return Wrapper;
23+
};
24+
25+
export default withAuth;

src/pages/logg-inn.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Layout from '@/components/Layout/Layout.component';
2+
import UserLogin from '@/components/User/UserLogin.component';
3+
4+
import type { NextPage } from 'next';
5+
6+
const LoginPage: NextPage = () => {
7+
return (
8+
<Layout title="Logg inn">
9+
<div className="container mx-auto px-4 py-8">
10+
<UserLogin />
11+
</div>
12+
</Layout>
13+
);
14+
};
15+
16+
export default LoginPage;

src/pages/min-konto.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Layout from '@/components/Layout/Layout.component';
2+
import CustomerAccount from '@/components/User/CustomerAccount.component';
3+
import withAuth from '@/components/User/withAuth.component';
4+
5+
import type { NextPage } from 'next';
6+
7+
const CustomerAccountPage: NextPage = () => {
8+
return (
9+
<Layout title="Min konto">
10+
<div className="container mx-auto px-4 py-8">
11+
<CustomerAccount />
12+
</div>
13+
</Layout>
14+
);
15+
};
16+
17+
export default withAuth(CustomerAccountPage);

src/utils/apollo/ApolloClient.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
/*eslint complexity: ["error", 6]*/
1+
/*eslint complexity: ["error", 8]*/
22

33
import {
44
ApolloClient,
55
InMemoryCache,
66
createHttpLink,
77
ApolloLink,
88
} from '@apollo/client';
9+
import { getAuthToken } from '../auth';
910

1011
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
1112

1213
/**
1314
* Middleware operation
1415
* If we have a session token in localStorage, add it to the GraphQL request as a Session header.
1516
*/
16-
export const middleware = new ApolloLink((operation, forward) => {
17+
export const middleware = new ApolloLink(async (operation, forward) => {
1718
/**
1819
* If session data exist in local storage, set value as session header.
1920
* Here we also delete the session if it is older than 7 days
@@ -22,6 +23,8 @@ export const middleware = new ApolloLink((operation, forward) => {
2223
? JSON.parse(localStorage.getItem('woo-session'))
2324
: null;
2425

26+
const headers = {};
27+
2528
if (sessionData && sessionData.token && sessionData.createdTime) {
2629
const { token, createdTime } = sessionData;
2730

@@ -32,14 +35,21 @@ export const middleware = new ApolloLink((operation, forward) => {
3235
localStorage.setItem('woocommerce-cart', JSON.stringify({}));
3336
} else {
3437
// If it's not, use the token
35-
operation.setContext(() => ({
36-
headers: {
37-
'woocommerce-session': `Session ${token}`,
38-
},
39-
}));
38+
headers['woocommerce-session'] = `Session ${token}`;
4039
}
4140
}
4241

42+
if (process.browser) {
43+
const authToken = await getAuthToken();
44+
if (authToken) {
45+
headers.Authorization = `Bearer ${authToken}`;
46+
}
47+
}
48+
49+
operation.setContext({
50+
headers,
51+
});
52+
4353
return forward(operation);
4454
});
4555

src/utils/auth.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ApolloClient, InMemoryCache } from '@apollo/client';
2+
import { LOGIN_USER } from './gql/GQL_MUTATIONS';
3+
4+
// Cookie-based authentication - no token storage needed
5+
export function hasCredentials() {
6+
if (typeof window === 'undefined') {
7+
return false; // Server-side, no credentials available
8+
}
9+
10+
// With cookie-based auth, we'll check if user is logged in through a query
11+
// For now, we'll return false and let components handle the check
12+
return false;
13+
}
14+
15+
export async function getAuthToken() {
16+
// Cookie-based auth doesn't need JWT tokens
17+
return null;
18+
}
19+
20+
export async function login(username: string, password: string) {
21+
try {
22+
const client = new ApolloClient({
23+
uri: process.env.NEXT_PUBLIC_GRAPHQL_URL,
24+
cache: new InMemoryCache(),
25+
credentials: 'include', // Include cookies in requests
26+
});
27+
28+
const { data } = await client.mutate({
29+
mutation: LOGIN_USER,
30+
variables: { username, password },
31+
});
32+
33+
const loginResult = data.loginWithCookies;
34+
35+
if (loginResult.status !== 'SUCCESS') {
36+
throw new Error('Login failed');
37+
}
38+
39+
// On successful login, cookies are automatically set by the server
40+
return { success: true, status: loginResult.status };
41+
} catch (error: unknown) {
42+
if (error instanceof Error) {
43+
throw new Error(error.message);
44+
}
45+
throw new Error('An unknown error occurred during login.');
46+
}
47+
}
48+
49+
export async function logout() {
50+
// For cookie-based auth, we might need a logout mutation
51+
// For now, we can clear any client-side state
52+
if (typeof window !== 'undefined') {
53+
// Redirect to login or home page after logout
54+
window.location.href = '/';
55+
}
56+
}

0 commit comments

Comments
 (0)