Skip to content

Commit

Permalink
fix: more fixes (#60)
Browse files Browse the repository at this point in the history
* fix: remove dev x cache header

* fix: access token expire not being set on refresh

* fix: prettier error messages, fix 404 unable to be shown

* fix: add allroutes

* [autofix.ci] apply automated fixes

* fix: add err handling for getAllUsernames

* fix: add err handling for admintable

* fix: make service session row compact

* [autofix.ci] apply automated fixes

* chore: install mc client with dockerfile

* feat: add backup task

* [autofix.ci] apply automated fixes

* fix: use coderabbit suggestion

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* [autofix.ci] apply automated fixes

* refactor: handling of minio task

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored May 1, 2024
1 parent ad67c3e commit 63903f5
Show file tree
Hide file tree
Showing 13 changed files with 156 additions and 50 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ dist
pgdata/
minio_data/
dump/
minio-dump/

*.csv
*.sql
3 changes: 3 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ services:
- type: bind
source: ./dump
target: /tmp/dump
- type: bind
source: ./minio-dump
target: /tmp/minio-dump

minio:
container_name: interapp-minio
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ services:
context: ./interapp-backend
dockerfile: scheduler/prod.Dockerfile
env_file:
- ./interapp-backend/.env.development
- ./interapp-backend/.env.production
networks:
- interapp-network
depends_on:
Expand All @@ -52,6 +52,7 @@ services:
condition: service_healthy
volumes:
- ./dump:/tmp/dump
- ./minio-dump:/tmp/minio-dump

minio:
container_name: interapp-minio
Expand Down
13 changes: 13 additions & 0 deletions interapp-backend/scheduler/dev.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg m
# update and install postgresql-client-16, tzdata
RUN apt-get update && apt-get install -y --fix-missing postgresql-client-16 tzdata && apt-get clean

RUN ARCH=$(case "$(uname -m)" in "x86_64") echo "amd64";; "ppc64le") echo "ppc64le";; *) echo "Unsupported architecture"; exit 1;; esac) && \
DOWNLOAD_URL=$(case "$ARCH" in "amd64") echo "https://dl.min.io/client/mc/release/linux-amd64/mc";; "ppc64le") echo "https://dl.min.io/client/mc/release/linux-ppc64le/mc";; *) echo "Unsupported architecture"; exit 1;; esac) && \
# Install wget to download MinIO client
apt-get update && apt-get install -y wget && \
# Download MinIO client binary
wget -O /usr/local/bin/mc "$DOWNLOAD_URL" && \
# Make MinIO client binary executable
chmod +x /usr/local/bin/mc && \
# Clean up
apt-get clean && rm -rf /var/lib/apt/lists/* && \
# Output success message
echo "MinIO client installed successfully."

ENV TZ=Asia/Singapore
RUN ln -snf /usr/share/zoneinfo/"$TZ" /etc/localtime && echo "$TZ" > /etc/timezone
RUN apt-get clean
Expand Down
13 changes: 13 additions & 0 deletions interapp-backend/scheduler/prod.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg m
# update and install postgresql-client-16, tzdata
RUN apt-get update && apt-get install -y --fix-missing postgresql-client-16 tzdata && apt-get clean

RUN ARCH=$(case "$(uname -m)" in "x86_64") echo "amd64";; "ppc64le") echo "ppc64le";; *) echo "Unsupported architecture"; exit 1;; esac) && \
DOWNLOAD_URL=$(case "$ARCH" in "amd64") echo "https://dl.min.io/client/mc/release/linux-amd64/mc";; "ppc64le") echo "https://dl.min.io/client/mc/release/linux-ppc64le/mc";; *) echo "Unsupported architecture"; exit 1;; esac) && \
# Install wget to download MinIO client
apt-get update && apt-get install -y wget && \
# Download MinIO client binary
wget -O /usr/local/bin/mc "$DOWNLOAD_URL" && \
# Make MinIO client binary executable
chmod +x /usr/local/bin/mc && \
# Clean up
apt-get clean && rm -rf /var/lib/apt/lists/* && \
# Output success message
echo "MinIO client installed successfully."

ENV TZ=Asia/Singapore
RUN ln -snf /usr/share/zoneinfo/"$TZ" /etc/localtime && echo "$TZ" > /etc/timezone
RUN apt-get clean
Expand Down
46 changes: 45 additions & 1 deletion interapp-backend/scheduler/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,55 @@ schedule('0 0 0 */1 * *', async () => {
await $`find ${path} -type f -mtime +7 -exec rm {} +`;

const d = new Date();
const fmted = `interapp_${d.toLocaleDateString('en-GB').replace(/\//g, '_')}`;
const fmted = `interapp_db_${d.toLocaleDateString('en-GB').replace(/\//g, '_')}`;

const newFile = `${path}/${fmted}.sql`;
await $`touch ${newFile}`;
await $`PGPASSWORD=postgres pg_dump -U postgres -a interapp -h interapp-postgres > ${newFile}`;

console.info('db snapshot taken at location: ', newFile);
});

// Minio backup task
const requiredEnv = [
'MINIO_ENDPOINT',
'MINIO_ADDRESS',
'MINIO_ROOT_USER',
'MINIO_ROOT_PASSWORD',
'MINIO_BUCKETNAME',
];
const missingEnv = requiredEnv.filter((env) => !process.env[env]);
if (missingEnv.length > 0) {
console.error('Missing required environment variables: ', missingEnv);
process.exit(1);
}

// define minio variables
const minioURL = `http://${process.env.MINIO_ENDPOINT}${process.env.MINIO_ADDRESS}`;
const minioAccessKey = process.env.MINIO_ROOT_USER as string;
const minioSecretKey = process.env.MINIO_ROOT_PASSWORD as string;
const minioBucketName = process.env.MINIO_BUCKETNAME as string;
const minioAliasName = 'minio';

const minioBackupTask = schedule(
'0 0 0 */1 * *',
async () => {
const path = '/tmp/minio-dump';
if (!existsSync(path)) mkdirSync(path);
// remove all files older than 7 days
await $`find ${path} -type f -mtime +7 -exec rm {} +`;

const d = new Date();
const fmted = `interapp_minio_${d.toLocaleDateString('en-GB').replace(/\//g, '_')}`;
const newFile = `${path}/${fmted}.tar.gz`;

await $`mc mirror ${minioAliasName}/${minioBucketName} /tmp/minio-dump/temp`;
await $`cd /tmp && tar -cvf ${newFile} minio-dump/temp`;
await $`rm -rf /tmp/minio-dump/temp`;
},
{ scheduled: false },
);

// set minio alias to allow mc to access minio server
await $`mc alias set ${minioAliasName} ${minioURL} ${minioAccessKey} ${minioSecretKey}`;
minioBackupTask.start();
26 changes: 22 additions & 4 deletions interapp-frontend/src/app/admin/AdminTable/AdminTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,32 @@ import DeleteAction from './DeleteAction/DeleteAction';
import PageController, { paginateItems } from '@components/PageController/PageController';
import { IconSearch } from '@tabler/icons-react';
import './styles.css';
import PageSkeleton from '@/components/PageSkeleton/PageSkeleton';
import PageSkeleton from '@components/PageSkeleton/PageSkeleton';
import { ClientError } from '@utils/.';

const fetchUserData = async () => {
const apiClient = new APIClient().instance;

const users: Omit<User, 'permissions'>[] = (await apiClient.get('/user')).data;
const perms: { [username: string]: Permissions[] } = (await apiClient.get('/user/permissions'))
.data;
const usersResponse = await apiClient.get('/user');
if (usersResponse.status !== 200)
throw new ClientError({
message: 'Failed to fetch users',
responseStatus: usersResponse.status,
responseBody: usersResponse.data,
});

const users: Omit<User, 'permissions'>[] = usersResponse.data;

const permsResponse = await apiClient.get('/user/permissions');
if (permsResponse.status !== 200)
throw new ClientError({
message: 'Failed to fetch permissions',
responseStatus: permsResponse.status,
responseBody: permsResponse.data,
});

const perms: { [username: string]: Permissions[] } = permsResponse.data;

const usersWithPerms = users.map((user) => ({ ...user, permissions: perms[user.username] }));
return usersWithPerms;
};
Expand Down
4 changes: 4 additions & 0 deletions interapp-frontend/src/app/route_permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ export const routePermissions = {
[Permissions.ATTENDANCE_MANAGER]: ['/exports'],
[Permissions.ADMIN]: ['/admin'],
} as const;

export const allRoutes = new Set<string>(
(Object.values(routePermissions).flat() as string[]).concat(noLoginRequiredRoutes),
);
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ const ServiceSessionRow = ({
<Table.Td>{service_name}</Table.Td>

<Table.Td>
{new Date(start_time).toLocaleString('en-GB')} -{' '}
{new Date(end_time).toLocaleString('en-GB')}{' '}
<Text size='sm'>Start: {new Date(start_time).toLocaleString('en-GB')}</Text>
<Text size='sm'>End: {new Date(end_time).toLocaleString('en-GB')}</Text>
</Table.Td>

<Table.Td>{ad_hoc_enabled ? <Text c='green'>Yes</Text> : <Text c='red'>No</Text>}</Table.Td>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
.service-session-img {
width: 100%;
height: 5rem;
height: 3.5rem;
object-fit: scale-down;
}

.service-session-users {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.2rem;
grid-template-columns: repeat(4, 1fr);
gap: 0.1rem;
justify-items: center;
}

Expand All @@ -17,6 +17,6 @@

@media (max-width: 768px) {
.service-session-users {
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(3, 1fr);
}
}
71 changes: 39 additions & 32 deletions interapp-frontend/src/providers/AuthProvider/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
} from './types';
import APIClient from '@api/api_client';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { routePermissions, noLoginRequiredRoutes } from '@/app/route_permissions';
import { routePermissions, noLoginRequiredRoutes, allRoutes } from '@/app/route_permissions';
import { notifications } from '@mantine/notifications';
import { ClientError, remapAssetUrl, wildcardMatcher } from '@utils/.';
import { remapAssetUrl, wildcardMatcher } from '@utils/.';

export const AuthContext = createContext<AuthContextType>({
user: null,
Expand All @@ -23,6 +23,25 @@ export const AuthContext = createContext<AuthContextType>({
registerUserAccount: async () => 0,
});

const authCheckErrors = {
INVALID_USER_TYPE: 'Invalid user type in local storage',
NOT_AUTHORISED: 'You must be logged in to access this page',
NO_PERMISSION: 'You do not have permission to access this page',
ALREADY_LOGGED_IN: 'You are already logged in. Redirecting to home page.',
} as const;

const showNotification = (title: keyof typeof authCheckErrors) => {
notifications.show({
// CAPS_CASE to Title Case
title: title
.toLowerCase()
.replace(/_/g, ' ')
.replace(/(^\w|\s\w)/g, (m) => m.toUpperCase()),
message: authCheckErrors[title],
color: 'red',
});
};

export const AuthProvider = ({ children }: AuthProviderProps) => {
const [user, setUser] = useState<User | null>(null);
const [justLoggedIn, setJustLoggedIn] = useState(false); // used to prevent redirecting to home page after login
Expand Down Expand Up @@ -55,27 +74,20 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
const validUser = validateUserType(user);
if (!validUser) {
logout();
notifications.show({
title: 'Error',
message: 'Invalid user type in local storage',
color: 'red',
});
/*
throw new ClientError({
message: 'Invalid user type in local storage' + JSON.stringify(user),
});
*/
showNotification('INVALID_USER_TYPE');
}

const disallowedRoutes = Array.from(allRoutes).filter(
(route) => !allowedRoutes.includes(route),
);

// check if the current route is allowed
if (allowedRoutes.some((route) => memoWildcardMatcher(pathname, route))) {
return;
}
// if the current route is not allowed, check if the user is logged in
if (!user) {
notifications.show({
title: 'Error',
message: 'You must be logged in to access this page',
color: 'red',
});
showNotification('NOT_AUTHORISED');
// convert search params to an object and then to a query string
const constructedSearchParams = Object.entries(Object.fromEntries(params))
.map(([key, value]) => `${key}=${value}`)
Expand All @@ -85,21 +97,17 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {

return;
}
// if the user is logged in and tries to access the login or signup page
if (user && (pathname === '/auth/login' || pathname === '/auth/signup')) {
notifications.show({
title: 'Info',
message: 'You are already logged in. Redirecting to home page.',
color: 'red',
});
showNotification('ALREADY_LOGGED_IN');
router.replace('/');
return;
}
notifications.show({
title: 'Error',
message: 'You do not have permission to access this page',
color: 'red',
});
router.replace('/');
// if the user is logged in but does not have permission to access the current route
if (disallowedRoutes.some((route) => memoWildcardMatcher(pathname, route))) {
showNotification('NO_PERMISSION');
router.replace('/');
}
}, [user, loading]);

useEffect(() => {
Expand All @@ -118,10 +126,9 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
logout();
return;
}
localStorage.setItem(
'access_token',
(res.data as Omit<UserWithJWT, 'user'>).access_token,
);
const data = res.data as Omit<UserWithJWT, 'user'>;
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('access_token_expire', data.expire.toString());
})
.catch(logout);
}
Expand Down
8 changes: 8 additions & 0 deletions interapp-frontend/src/utils/getAllUsernames.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { APIClient } from '@api/api_client';
import { User } from '@providers/AuthProvider/types';
import { ClientError } from './parseClientError';

export async function getAllUsernames() {
const apiClient = new APIClient().instance;

const get_all_users = await apiClient.get('/user');
if (get_all_users.status !== 200)
throw new ClientError({
message: 'Failed to get all users',
responseStatus: get_all_users.status,
responseBody: get_all_users.data,
});

const all_users: Omit<User, 'permissions'>[] = get_all_users.data;
const allUsersNames = all_users !== undefined ? all_users.map((user) => user.username) : [];
return allUsersNames;
Expand Down
6 changes: 0 additions & 6 deletions nginx/default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,13 @@ server {
location /_next/static {
proxy_cache STATIC;
proxy_pass http://frontend;

# For testing cache - remove before deploying to production
add_header X-Cache-Status $upstream_cache_status;
}

location /static {
proxy_cache STATIC;
proxy_ignore_headers Cache-Control;
proxy_cache_valid 60m;
proxy_pass http://frontend;

# For testing cache - remove before deploying to production
add_header X-Cache-Status $upstream_cache_status;
}

location /api {
Expand Down

0 comments on commit 63903f5

Please sign in to comment.