diff --git a/components/Layout/index.tsx b/components/Layout/index.tsx index 398a51f..6e1dd85 100644 --- a/components/Layout/index.tsx +++ b/components/Layout/index.tsx @@ -20,7 +20,7 @@ import AdminNavigation from '../AdminNavigation'; import {logout} from '@/redux/auth/action/logout'; import {useRouter} from 'next/router'; import getConfig from 'next/config'; -import {Account} from '@/types/Account'; +import {SessionAccount} from '@/types/Account'; const {publicRuntimeConfig} = getConfig(); @@ -56,7 +56,7 @@ const Layout: React.FunctionComponent> = ({ }, [user, dispatch]); const handleAccountSwitch = React.useCallback( - (account: Account) => { + (account: SessionAccount) => { setAnchorEl(null); router.push(`${publicRuntimeConfig.appURL}/dashboard/${account._id}`); }, diff --git a/helpers/createJWT.ts b/helpers/createJWT.ts index 4a12370..25de93b 100644 --- a/helpers/createJWT.ts +++ b/helpers/createJWT.ts @@ -13,12 +13,6 @@ const createJWT: CreateJWT = async (user) => { { _id, role: user.role, - email: user.email, - emailVerified: user.emailVerified, - accounts: user.accounts, - maxAccounts: user.maxAccounts, - serverURLOnSignUp: user.serverURLOnSignUp, - timezone: user.timezone, }, serverRuntimeConfig.jwtSecret, {expiresIn: '10m'}, diff --git a/helpers/createSessionUser.ts b/helpers/createSessionUser.ts new file mode 100644 index 0000000..b0fe5c6 --- /dev/null +++ b/helpers/createSessionUser.ts @@ -0,0 +1,31 @@ +import {Account, SessionAccount} from '@/types/Account'; +import {SessionUser, User} from '@/types/User'; + +const createSessionAccount = (account: Account): SessionAccount => { + return { + _id: account._id.toString(), + serverURL: account.serverURL, + name: account.name, + username: account.username, + accountName: account.accountName, + accountURL: account.accountURL, + avatarURL: account.avatarURL, + utcOffset: account.utcOffset, + timezone: account.timezone, + }; +}; + +const createSessionUser = (user: User): SessionUser => { + return { + _id: user._id.toString(), + role: user.role, + email: user.email, + emailVerified: user.emailVerified, + accounts: user.accounts?.map(createSessionAccount), + maxAccounts: user.maxAccounts, + serverURLOnSignUp: user.serverURLOnSignUp, + timezone: user.timezone, + }; +}; + +export default createSessionUser; diff --git a/helpers/getAuthInfoFromRequest.ts b/helpers/getAuthInfoFromRequest.ts index 84096ea..9e2a0cd 100644 --- a/helpers/getAuthInfoFromRequest.ts +++ b/helpers/getAuthInfoFromRequest.ts @@ -2,11 +2,11 @@ import jwt from 'jsonwebtoken'; import getConfig from 'next/config'; import refreshUser from '@/service/authentication/refreshUser'; import {NextApiRequest} from 'next'; -import {User} from '../types/User'; +import {JwtUser} from '../types/User'; const {serverRuntimeConfig} = getConfig(); -type GetAuthInfoFromRequest = (req: NextApiRequest, forceRefresh?: boolean) => Promise<{user?: User; token?: string; refreshToken?: string}>; +type GetAuthInfoFromRequest = (req: NextApiRequest, forceRefresh?: boolean) => Promise<{user?: JwtUser; token?: string; refreshToken?: string}>; const getAuthInfoFromRequest: GetAuthInfoFromRequest = async ({cookies, headers}, forceRefresh = false) => { const token = headers['authorization']?.split(' ')[1] ?? cookies.token ?? ''; try { @@ -14,27 +14,12 @@ const getAuthInfoFromRequest: GetAuthInfoFromRequest = async ({cookies, headers} throw new Error(); } - const { - _id, - role, - email, - emailVerified = false, - accounts = null, - maxAccounts = null, - serverURLOnSignUp = null, - timezone = null, - } = (await jwt.verify(token, serverRuntimeConfig.jwtSecret)) as Partial; + const {_id, role} = (await jwt.verify(token, serverRuntimeConfig.jwtSecret)) as JwtUser; return { user: { _id, role, - email, - emailVerified, - accounts, - maxAccounts, - serverURLOnSignUp, - timezone, - } as User, + } as JwtUser, token, }; } catch (error: any) { @@ -43,28 +28,13 @@ const getAuthInfoFromRequest: GetAuthInfoFromRequest = async ({cookies, headers} if (response === null) { return {}; } - const { - _id, - role, - email, - emailVerified = false, - accounts = null, - maxAccounts = null, - serverURLOnSignUp = null, - timezone = null, - } = jwt.decode(response.token) as Partial; + const {_id, role} = jwt.decode(response.token) as JwtUser; return { + ...response, user: { _id, role, - email, - emailVerified, - accounts, - maxAccounts, - serverURLOnSignUp, - timezone, - } as User, - ...response, + } as JwtUser, }; } else { console.warn(error?.message); diff --git a/helpers/handleAuthentication.ts b/helpers/handleAuthentication.ts index e31f119..5dd2ea6 100644 --- a/helpers/handleAuthentication.ts +++ b/helpers/handleAuthentication.ts @@ -5,6 +5,11 @@ import getAuthInfoFromRequest from '@/helpers/getAuthInfoFromRequest'; import {UserRole} from '@/types/UserRole'; import {AuthResponseType} from '@/types/AuthResponseType'; import {loginSuccessful} from '@/redux/auth/slice'; +import UserModel from '@/models/UserModel'; +import AccountModel from '@/models/AccountModel'; +import UserCredentialsModel from '@/models/UserCredentialsModel'; +import {logger} from './logger'; +import createSessionUser from './createSessionUser'; type HandleAuthentication = ( roles: UserRole[], @@ -13,16 +18,27 @@ type HandleAuthentication = ( ) => Promise<{id?: string; role?: string; token?: string}>; const handleAuthentication: HandleAuthentication = async (roles, type, {store, req, res, forceRefresh = false}) => { - const {user, token, refreshToken} = await getAuthInfoFromRequest(req as NextApiRequest, forceRefresh); + const {user: jwtUser, token, refreshToken} = await getAuthInfoFromRequest(req as NextApiRequest, forceRefresh); - if (refreshToken && token) { - res.setHeader('Set-Cookie', [ - serialize('token', token, {path: '/', httpOnly: true, secure: process.env.NODE_ENV !== 'development'}), - serialize('refreshToken', refreshToken, {path: '/', httpOnly: true, secure: process.env.NODE_ENV !== 'development'}), - ]); + if (!jwtUser || !roles.map((role) => role.toString()).includes(jwtUser.role)) { + switch (type) { + case AuthResponseType.Error: + res.status(401).end(); + break; + default: + res.writeHead(302, {Location: '/login'}).end(); + break; + } + return {}; } - if (!user || !roles.map((role) => role.toString()).includes(user.role)) { + const user = await UserModel.findById(jwtUser._id).populate([ + {path: 'accounts', model: AccountModel, match: {setupComplete: true}}, + {path: 'credentials', model: UserCredentialsModel}, + ]); + + if (!user) { + logger.error(`handleAuthentication: User not found: ${jwtUser._id}`); switch (type) { case AuthResponseType.Error: res.status(401).end(); @@ -32,10 +48,17 @@ const handleAuthentication: HandleAuthentication = async (roles, type, {store, r break; } return {}; - } else { - store?.dispatch(loginSuccessful(user)); - return {id: user._id, role: user.role, token}; } + + if (refreshToken && token) { + res.setHeader('Set-Cookie', [ + serialize('token', token, {path: '/', httpOnly: true, secure: process.env.NODE_ENV !== 'development'}), + serialize('refreshToken', refreshToken, {path: '/', httpOnly: true, secure: process.env.NODE_ENV !== 'development'}), + ]); + } + + store?.dispatch(loginSuccessful(createSessionUser(user))); + return {id: user._id, role: user.role, token}; }; export default handleAuthentication; diff --git a/pages/api/user/login.ts b/pages/api/user/login.ts index aa1350e..901a37e 100644 --- a/pages/api/user/login.ts +++ b/pages/api/user/login.ts @@ -16,13 +16,13 @@ const login: Login = async ({body, method}, res) => { return res.status(401).end(); } - const {token, refreshToken} = response; + const {token, refreshToken, user} = response; res.setHeader('Set-Cookie', [ serialize('token', token, {path: '/', httpOnly: true, secure: process.env.NODE_ENV !== 'development'}), serialize('refreshToken', refreshToken, {path: '/', httpOnly: true, secure: process.env.NODE_ENV !== 'development'}), ]); - res.json({token, refreshToken}); + res.json({token, refreshToken, user}); }; export default login; diff --git a/pages/api/user/refresh.ts b/pages/api/user/refresh.ts index 816815f..a59448f 100644 --- a/pages/api/user/refresh.ts +++ b/pages/api/user/refresh.ts @@ -17,13 +17,13 @@ const refresh: Refresh = async ({cookies, method}, res) => { return res.status(400).end(); } - const {token, refreshToken} = response; + const {token, refreshToken, user} = response; res.setHeader('Set-Cookie', [ serialize('token', token, {path: '/', httpOnly: true, secure: process.env.NODE_ENV !== 'development'}), serialize('refreshToken', refreshToken, {path: '/', httpOnly: true, secure: process.env.NODE_ENV !== 'development'}), ]); - res.json({token, refreshToken}); + res.json({token, refreshToken, user}); }; export default refresh; diff --git a/pages/api/user/register.ts b/pages/api/user/register.ts index d47731f..589fa5b 100644 --- a/pages/api/user/register.ts +++ b/pages/api/user/register.ts @@ -71,13 +71,13 @@ const register: Register = async ({body, method}, res) => { return res.status(500).end(); } - const {token, refreshToken} = response; + const {token, refreshToken, user: sessionUser} = response; res.setHeader('Set-Cookie', [ serialize('token', token, {path: '/', httpOnly: true, secure: process.env.NODE_ENV !== 'development'}), serialize('refreshToken', refreshToken, {path: '/', httpOnly: true, secure: process.env.NODE_ENV !== 'development'}), ]); - res.json({token, refreshToken}); + res.json({token, refreshToken, user: sessionUser}); }; export default register; diff --git a/redux/auth/action/login.ts b/redux/auth/action/login.ts index 45ce554..1288999 100644 --- a/redux/auth/action/login.ts +++ b/redux/auth/action/login.ts @@ -1,7 +1,5 @@ import postJSON from '@/helpers/postJSON'; import {AppDispatch} from '@/redux/store'; -import {User} from '@/types/User'; -import jwt from 'jsonwebtoken'; import {loginAttempt, loginFailed, loginSuccessful} from '../slice'; export const login = (email: string, password: string) => async (dispatch: AppDispatch) => { @@ -10,8 +8,7 @@ export const login = (email: string, password: string) => async (dispatch: AppDi const response = await postJSON('/api/user/login', {email, password}); if (response.status === 200) { - const {token} = await response.data; - const user = jwt.decode(token) as User; + const {user} = await response.data; dispatch(loginSuccessful(user)); } else { dispatch(loginFailed('Failed to log in. User or password invalid. Please try again.')); diff --git a/redux/auth/action/logout.ts b/redux/auth/action/logout.ts index 2285ab8..7ae8791 100644 --- a/redux/auth/action/logout.ts +++ b/redux/auth/action/logout.ts @@ -1,9 +1,9 @@ import postJSON from '@/helpers/postJSON'; import {AppDispatch} from '@/redux/store'; -import {User} from '@/types/User'; +import {SessionUser} from '@/types/User'; import {logoutSuccessful} from '../slice'; -export const logout = (user: User) => async (dispatch: AppDispatch) => { +export const logout = (user: SessionUser) => async (dispatch: AppDispatch) => { await postJSON('/api/user/logout', user); dispatch(logoutSuccessful()); }; diff --git a/redux/auth/action/refresh.ts b/redux/auth/action/refresh.ts index 26bdbc5..1698642 100644 --- a/redux/auth/action/refresh.ts +++ b/redux/auth/action/refresh.ts @@ -1,15 +1,12 @@ import get from '@/helpers/get'; import {AppDispatch} from '@/redux/store'; -import {User} from '@/types/User'; -import jwt from 'jsonwebtoken'; import {loginSuccessful} from '../slice'; export const refresh = () => async (dispatch: AppDispatch) => { const response = await get('/api/user/refresh'); if (response.status === 200) { - const {token} = await response.data; - const user = jwt.decode(token) as User; + const {user} = await response.data; dispatch(loginSuccessful(user)); } }; diff --git a/redux/auth/slice.ts b/redux/auth/slice.ts index 704a770..420da05 100644 --- a/redux/auth/slice.ts +++ b/redux/auth/slice.ts @@ -1,12 +1,12 @@ import {createSlice, PayloadAction} from '@reduxjs/toolkit'; -import {User} from '@/types/User'; +import {SessionUser} from '@/types/User'; import {HYDRATE} from 'next-redux-wrapper'; -import {Account} from '@/types/Account'; +import {SessionAccount} from '@/types/Account'; // Define a type for the slice state interface AuthState { - user?: User | null; - account?: Account | null; + user?: SessionUser | null; + account?: SessionAccount | null; loginInProgress: boolean; loginError?: string | null; resetPasswordRequestInProgress?: boolean; @@ -27,7 +27,7 @@ export const authSlice = createSlice({ state.loginInProgress = true; state.loginError = null; }, - loginSuccessful: (state, action: PayloadAction) => { + loginSuccessful: (state, action: PayloadAction) => { state.user = action.payload; state.account = (action.payload.accounts?.length ?? 0) > 0 ? action.payload.accounts![0] : null; state.loginInProgress = false; @@ -41,7 +41,7 @@ export const authSlice = createSlice({ state.user = null; state.account = null; }, - switchAccount: (state, action: PayloadAction) => { + switchAccount: (state, action: PayloadAction) => { state.account = action.payload; }, resetPasswordRequestStarted: (state) => { diff --git a/redux/user/action/register.ts b/redux/user/action/register.ts index 52162e0..c674aa5 100644 --- a/redux/user/action/register.ts +++ b/redux/user/action/register.ts @@ -2,8 +2,6 @@ import postJSON from '@/helpers/postJSON'; import {loginSuccessful} from '@/redux/auth/slice'; import {AppDispatch} from '@/redux/store'; import {RegistrationFormData} from '@/schemas/registrationForm'; -import {User} from '@/types/User'; -import jwt from 'jsonwebtoken'; import {registrationAttempt, registrationFailed} from '../slice'; export const register = (data: RegistrationFormData) => async (dispatch: AppDispatch) => { @@ -12,8 +10,7 @@ export const register = (data: RegistrationFormData) => async (dispatch: AppDisp const response = await postJSON('/api/user/register', data); if (response.status === 200) { - const {token} = await response.data; - const user = jwt.decode(token) as User; + const {user} = await response.data; dispatch(loginSuccessful(user)); } else { const {error} = await response.data; diff --git a/service/authentication/loginUser.ts b/service/authentication/loginUser.ts index 2ba3161..b0f71c8 100644 --- a/service/authentication/loginUser.ts +++ b/service/authentication/loginUser.ts @@ -4,8 +4,10 @@ import dbConnect from '@/helpers/dbConnect'; import UserModel from '@/models/UserModel'; import UserCredentialsModel, {UserCredentials} from '@/models/UserCredentialsModel'; import AccountModel from '@/models/AccountModel'; +import {SessionUser} from '@/types/User'; +import createSessionUser from '@/helpers/createSessionUser'; -type LoginUser = (email: string, password: string) => Promise<{token: string; refreshToken: string} | null>; +type LoginUser = (email: string, password: string) => Promise<{token: string; refreshToken: string; user: SessionUser} | null>; const loginUser: LoginUser = async (email, password) => { await dbConnect(); @@ -35,7 +37,7 @@ const loginUser: LoginUser = async (email, password) => { user.oldAccountDeletionNoticeSent = false; await user.save(); - return {token, refreshToken}; + return {token, refreshToken, user: createSessionUser(user)}; }; export default loginUser; diff --git a/service/authentication/refreshUser.ts b/service/authentication/refreshUser.ts index fcb501e..dbd69cd 100644 --- a/service/authentication/refreshUser.ts +++ b/service/authentication/refreshUser.ts @@ -1,10 +1,12 @@ import createJWT from '@/helpers/createJWT'; +import createSessionUser from '@/helpers/createSessionUser'; import dbConnect from '@/helpers/dbConnect'; import AccountModel from '@/models/AccountModel'; import UserCredentialsModel from '@/models/UserCredentialsModel'; import UserModel from '@/models/UserModel'; +import {SessionUser} from '@/types/User'; -type RefreshUser = (refreshToken: string) => Promise<{token: string; refreshToken: string} | null>; +type RefreshUser = (refreshToken: string) => Promise<{token: string; refreshToken: string; user: SessionUser} | null>; const refreshUser: RefreshUser = async (currentRefreshToken) => { await dbConnect(); @@ -27,7 +29,7 @@ const refreshUser: RefreshUser = async (currentRefreshToken) => { user.oldAccountDeletionNoticeSent = false; await user.save(); - return {token, refreshToken}; + return {token, refreshToken, user: createSessionUser(user)}; }; export default refreshUser; diff --git a/types/Account.ts b/types/Account.ts index 2bf1d32..a2420ff 100644 --- a/types/Account.ts +++ b/types/Account.ts @@ -19,3 +19,15 @@ export interface Account { createdAt: Date | string; updatedAt: Date | string; } + +export interface SessionAccount { + _id: string; + serverURL: string; + name?: string; + username?: string; + accountName?: string; + accountURL?: string; + avatarURL?: string; + utcOffset: string; + timezone: string; +} diff --git a/types/User.ts b/types/User.ts index 9e63653..9e33011 100644 --- a/types/User.ts +++ b/types/User.ts @@ -1,4 +1,4 @@ -import {Account} from './Account'; +import {Account, SessionAccount} from './Account'; import {UserCredentials} from './UserCredentials'; export interface User { @@ -19,3 +19,19 @@ export interface User { createdAt: Date | string; updatedAt: Date | string; } + +export interface JwtUser { + _id: string; + role: string; +} + +export interface SessionUser { + _id: string; + role: string; + email: string; + emailVerified: boolean; + accounts?: SessionAccount[]; + maxAccounts?: number; + serverURLOnSignUp?: string; + timezone?: string; +}