Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,18 @@ export const icons = {
cog: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" fillRule="evenodd" /></svg>,
download: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" fillRule="evenodd" /></svg>,
refresh: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clipRule="evenodd" /></svg>,
users: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" /></svg>,
trash: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" /></svg>,
key: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clipRule="evenodd" /></svg>,
upload: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM6.293 6.707a1 1 0 010-1.414l3-3a1 1 0 011.414 0l3 3a1 1 0 01-1.414 1.414L11 5.414V13a1 1 0 11-2 0V5.414L7.707 6.707a1 1 0 01-1.414 0z" clipRule="evenodd" /></svg>,
calculator: (
<svg aria-hidden className={iconClassName} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<rect x="4" y="3" width="16" height="18" rx="2" strokeWidth="2"/>
<rect x="8" y="7" width="8" height="2" rx="1" strokeWidth="2"/>
<circle cx="9" cy="13" r="1" fill="currentColor"/>
<circle cx="15" cy="13" r="1" fill="currentColor"/>
<circle cx="9" cy="17" r="1" fill="currentColor"/>
<circle cx="15" cy="17" r="1" fill="currentColor"/>
</svg>
),
} as const;
10 changes: 9 additions & 1 deletion app/const/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@ export const localization = {
deleteDialogTitle: 'Delete Database?',
deleteDialogMessage: (database: string) =>
`Are you sure you want to delete "${database}" database?`,
makeSuperUser: 'Make super user',
makeSuperUser: 'Make Institution Admin',
resetPasswords: 'Reset Passwords',
resetPasswordsDialogTitle: 'Reset All Passwords?',
resetPasswordsDialogMessage: (database: string) =>
`Are you sure you want to reset all user passwords in the "${database}" database?`,
resettingPasswords: 'Resetting passwords...',
passwordsReset: 'All passwords have been reset successfully',
failedToResetPasswords: 'Failed to reset passwords',
calculateSizes: 'Calculate sizes',
uploadNewDatabase: 'Upload new database',
databaseName: 'Database Name',
Expand All @@ -79,6 +86,7 @@ export const localization = {
mb: 'MB',
nameConflict:
'Database with this name already exists. Please delete it first',
finalDatabaseName: 'Final database name:',
container: 'Container',
noLogsAvailable: 'No logs available for this container',
failedToDownload: 'Failed to download',
Expand Down
11 changes: 11 additions & 0 deletions app/lib/databaseNameHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Generate a database name with today's date suffix
* Format: {baseName}_YYYY_MM_DD
*/
export function generateDatabaseNameWithDate(baseName: string): string {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${baseName}_${year}_${month}_${day}`;
}
19 changes: 19 additions & 0 deletions app/lib/passwordUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Connection } from 'mysql2/promise';

const testPassword = 'EC62DEF08F5E4FD556DAA86AEC5F3FB0390EF8A862A41ECA';

/**
* Resets all user passwords in the specified database
*/
export async function resetDatabasePasswords(
connection: Connection,
databaseName: string
): Promise<void> {
await connection.execute(
`UPDATE \`${databaseName}\`.specifyuser
SET Password=?;`,
[testPassword]
);
}

export { testPassword };
26 changes: 13 additions & 13 deletions app/pages/api/databases/[name]/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,12 @@ export default async function handler(
const sanitizedFilename = String(databaseName).replace(/[^\w.-]/g, '_');

try {
res.setHeader(
'Content-Disposition',
`attachment; filename="${sanitizedFilename}.sql"`
);

const child = spawn(
'mysqldump',
const result = await spawn(
'mariadb-dump',
[
`-u${process.env.MYSQL_USERNAME}`,
`-p${process.env.MYSQL_PASSWORD}`,
`-h${process.env.MYSQL_HOST}`,
`--user=${process.env.MYSQL_USERNAME}`,
`--password=${process.env.MYSQL_PASSWORD}`,
`--host=${process.env.MYSQL_HOST}`,
`--databases ${databaseName}`,
'--no-create-db',
],
Expand All @@ -44,11 +39,16 @@ export default async function handler(
}
);

child.stdout.pipe(res);
child.stderr.on('data', (error) => {
res.setHeader(
'Content-Disposition',
`attachment; filename="${sanitizedFilename}.sql"`
);

result.stdout.pipe(res);
result.stderr.on('data', (error) => {
throw new Error(error);
});
await new Promise((resolve) => child.stdout.on('exit', resolve));
await new Promise((resolve) => result.stdout.on('exit', resolve));
} catch (error) {
res.status(500).json({
error: (error as object).toString(),
Expand Down
40 changes: 40 additions & 0 deletions app/pages/api/databases/[name]/reset-passwords.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { NextApiRequest, NextApiResponse } from 'next';

import { getUser } from '../../../../lib/apiUtils';
import { connectToDatabase } from '../../../../lib/database';
import { resetDatabasePasswords } from '../../../../lib/passwordUtils';

export default async function handler(
request: NextApiRequest,
res: NextApiResponse
) {
if (request.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}

const user = await getUser(request, res);
if (typeof user === 'undefined') return;

const databaseName = request.query.name as string;

if (!databaseName) {
return res.status(400).json({ error: 'Database name is required' });
}

// Validate database name to prevent SQL injection
if (databaseName.match(/^\w+$/) === null) {
return res.status(400).json({ error: 'Database name is invalid' });
}

try {
const connection = await connectToDatabase();

// Reset all user passwords to the test password
await resetDatabasePasswords(connection, databaseName);

res.status(200).json({ success: true });
} catch (error: any) {
console.error('Failed to reset passwords:', error);
res.status(500).json({ error: error.toString() });
}
}
20 changes: 9 additions & 11 deletions app/pages/api/databases/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import path from 'node:path';

import { getUser, run } from '../../../lib/apiUtils';
import { connectToDatabase } from '../../../lib/database';
import { generateDatabaseNameWithDate } from '../../../lib/databaseNameHelper';
import { resetDatabasePasswords } from '../../../lib/passwordUtils';

// First we need to disable the default body parser
export const config = {
Expand All @@ -14,8 +16,6 @@ export const config = {
},
};

const testuserPassword = 'EC62DEF08F5E4FD556DAA86AEC5F3FB0390EF8A862A41ECA';

export default async function handler(
request: NextApiRequest,
res: NextApiResponse
Expand Down Expand Up @@ -45,14 +45,16 @@ export default async function handler(

if (typeof data === 'string') return res.status(400).json({ error: data });

const databaseName = data.fields.databaseName as string | undefined;
const databaseNameForm = data.fields.databaseName as string | undefined;

if (!databaseName)
if (!databaseNameForm)
return res.status(400).json({ error: 'Database name is required' });

if (databaseName.match(/^\w+$/) === null)
if (databaseNameForm.match(/^\w+$/) === null)
return res.status(400).json({ error: 'Database name is invalid' });

const databaseName = generateDatabaseNameWithDate(databaseNameForm);

const file = data.files.file as File | undefined;

if (typeof file === 'undefined' || file.newFilename === null)
Expand Down Expand Up @@ -111,17 +113,13 @@ export default async function handler(
await connection.execute(`CREATE DATABASE \`${databaseName}\``);
await run(
[
`mysql -u${process.env.MYSQL_USERNAME} `,
`mariadb -u${process.env.MYSQL_USERNAME} `,
`-p${process.env.MYSQL_PASSWORD} `,
`-h${process.env.MYSQL_HOST} `,
`--database "${databaseName}" < ${filePath}`,
].join('')
);
await connection.execute(
`UPDATE \`${databaseName}\`.specifyuser
SET Password=?;`,
[testuserPassword]
);
await resetDatabasePasswords(connection, databaseName);
res.writeHead(302, {
Location: '/databases/',
});
Expand Down
10 changes: 9 additions & 1 deletion app/pages/api/dockerhub/[image].ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ type Response = {
readonly name: string;
readonly last_updated: string;
readonly digest: string;
readonly images: RA<{
readonly architecture: string;
}>;
}>;
readonly next: string | undefined;
};
Expand All @@ -35,7 +38,12 @@ const processTagsResponse = (tags: Response['results']): IR<DockerHubTag> =>
Object.fromEntries(
tags
// Latest is an unpredictable branch, thus will exclude it
.filter(({ name }) => !name.startsWith('sha-') && name !== 'latest')
.filter(
({ name, images }) =>
!name.startsWith('sha-') &&
name !== 'latest' &&
images.some(({ architecture }) => architecture === 'arm64')
)
.map(({ name, last_updated, digest }) => [
name,
{
Expand Down
Loading
Loading