Skip to content

Commit

Permalink
Merge pull request #165 from complexdatacollective/next
Browse files Browse the repository at this point in the history
v2.0.0
  • Loading branch information
jthrilly authored Nov 13, 2024
2 parents 7b10a0a + ff8be33 commit 9f563c6
Show file tree
Hide file tree
Showing 83 changed files with 2,426 additions and 1,462 deletions.
5 changes: 1 addition & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,15 @@
# Optional environment variables - uncomment to use
# -------------------

#DISABLE_ANALYTICS # true or false - If true, the app will not send anonymous analytics and error data. Defaults to false.
#SANDBOX_MODE=false # true or false - if true, the app will use the sandbox mode, which disables resetting the database and other features
#PUBLIC_URL="http://yourdomain.com" # When using advanced deployment, this is required. Set to the domain name of your app
#DISABLE_ANALYTICS=true # true or false - if true, the app will not send anonymous analytics data to the server
#INSTALLATION_ID="your-app-name" # A unique identifier for your app, used for analytics. Generated automatically if not set.

# -------------------
# Required environment variables
# -------------------

UPLOADTHING_SECRET=sk_live_xxxxxx # Your UploadThing secret key
UPLOADTHING_APP_ID=xxxxxxx # Your UploadThing app ID

POSTGRES_USER="postgres" # Your PostgreSQL username
POSTGRES_PASSWORD="postgres" # Your PostgreSQL password
POSTGRES_DATABASE="postgres" # Your PostgreSQL database name
Expand Down
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ WORKDIR /app
COPY prisma ./prisma

# Copy package.json and lockfile, along with postinstall script
COPY package.json pnpm-lock.yaml* postinstall.js migrate-and-start.sh handle-migrations.js ./
COPY package.json pnpm-lock.yaml* postinstall.js migrate-and-start.sh setup-database.js initialize.js ./

# # Install pnpm and install dependencies
RUN corepack enable pnpm && pnpm i --frozen-lockfile
Expand Down Expand Up @@ -55,7 +55,8 @@ RUN chown nextjs:nodejs .next
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/handle-migrations.js ./
COPY --from=builder --chown=nextjs:nodejs /app/initialize.js ./
COPY --from=builder --chown=nextjs:nodejs /app/setup-database.js ./
COPY --from=builder --chown=nextjs:nodejs /app/migrate-and-start.sh ./
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma

Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ add new features to Network Canvas, but rather provides a new way to conduct int

Read our [documentation](https://documentation.networkcanvas.com/en/fresco) for more information on deploying Fresco.

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcomplexdatacollective%2Ffresco%2Ftree%2Fmain&project-name=fresco&repository-name=fresco&demo-title=Network%20Canvas%20Fresco&demo-description=The%20Fresco%20project%20brings%20Network%20Canvas%20interviews%20to%20the%20web%20browser.%20See%20the%20Network%20Canvas%20project%20documentation%20website%20for%20more%20information.&demo-url=https%3A%2F%2Ffresco-sandbox.networkcanvas.com%2F&demo-image=https%3A%2F%2Fdocumentation.networkcanvas.com%2Fassets%2Fimg%2Ffresco-images%2Ffeatures%2Fdashboard.png&stores=%5B%7B"type"%3A"postgres"%7D%5D&env=UPLOADTHING_SECRET,UPLOADTHING_APP_ID&envDescription=The%20Uploadthing%20secret%20key%20and%20app%20ID%20let%20Fresco%20securely%20communicate%20with%20your%20data%20storage%20bucket.&envLink=https%3A%2F%2Fuploadthing.com%2Fdashboard%2F)

![Alt](https://repobeats.axiom.co/api/embed/3902b97960b7e32971202cbd5b0d38f39d51df51.svg "Repobeats analytics image")

## Building and Publishing Docker Images
Expand Down
67 changes: 33 additions & 34 deletions actions/appSettings.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,50 @@
'use server';

import { redirect } from 'next/navigation';
import { type z } from 'zod';
import { safeRevalidateTag } from '~/lib/cache';
import { type AppSetting, appSettingsSchema } from '~/schemas/appSettings';
import { requireApiAuth } from '~/utils/auth';
import { prisma } from '~/utils/db';
import { ensureError } from '~/utils/ensureError';

export async function setAnonymousRecruitment(input: boolean) {
await requireApiAuth();

await prisma.appSettings.updateMany({
data: {
allowAnonymousRecruitment: input,
},
});

safeRevalidateTag('allowAnonymousRecruitment');

return input;
}
// Convert string | boolean | Date to string
const getStringValue = (value: string | boolean | Date) => {
if (typeof value === 'boolean') return value.toString();
if (value instanceof Date) return value.toISOString();
return value;
};

export async function setLimitInterviews(input: boolean) {
export async function setAppSetting<
Key extends AppSetting,
V extends z.infer<typeof appSettingsSchema>[Key],
>(key: Key, value: V): Promise<V> {
await requireApiAuth();
await prisma.appSettings.updateMany({
data: {
limitInterviews: input,
},
});

safeRevalidateTag('limitInterviews');

return input;
}

export const setAppConfigured = async () => {
await requireApiAuth();
if (!appSettingsSchema.shape[key]) {
throw new Error(`Invalid app setting: ${key}`);
}

try {
await prisma.appSettings.updateMany({
data: {
configured: true,
},
const result = appSettingsSchema.shape[key].parse(value);
const stringValue = getStringValue(result);

await prisma.appSettings.upsert({
where: { key },
create: { key, value: stringValue },
update: { value: stringValue },
});

safeRevalidateTag('appSettings');
safeRevalidateTag(`appSettings-${key}`);

return value;
} catch (error) {
return { error: 'Failed to update appSettings', appSettings: null };
const e = ensureError(error);
throw new Error(`Failed to update appSettings: ${key}: ${e.message}`);
}
}

redirect('/dashboard');
};
export async function submitUploadThingForm(token: string) {
await setAppSetting('uploadThingToken', token);
redirect('/setup?step=3');
}
5 changes: 2 additions & 3 deletions actions/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { revalidatePath } from 'next/cache';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { createUserFormDataSchema, loginSchema } from '~/schemas/auth';
import { auth, getServerSession } from '~/utils/auth';
import { prisma } from '~/utils/db';
Expand Down Expand Up @@ -52,9 +53,7 @@ export async function signup(formData: unknown) {
sessionCookie.attributes,
);

return {
success: true,
};
redirect('/setup?step=2');
} catch (error) {
// db error, email taken, etc
return {
Expand Down
7 changes: 5 additions & 2 deletions actions/interviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
ExportReturn,
FormattedSession,
} from '~/lib/network-exporters/utils/types';
import { getAppSetting } from '~/queries/appSettings';
import { getInterviewsForExport } from '~/queries/interviews';
import type {
CreateInterview,
Expand Down Expand Up @@ -158,8 +159,10 @@ export async function createInterview(data: CreateInterview) {

try {
if (!participantIdentifier) {
const appSettings = await prisma.appSettings.findFirst();
if (!appSettings || !appSettings.allowAnonymousRecruitment) {
const allowAnonymousRecruitment = await getAppSetting(
'allowAnonymousRecruitment',
);
if (!allowAnonymousRecruitment) {
return {
errorType: 'no-anonymous-recruitment',
error: 'Anonymous recruitment is not enabled',
Expand Down
4 changes: 2 additions & 2 deletions actions/protocols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { type Protocol } from '@codaco/shared-consts';
import { Prisma } from '@prisma/client';
import { safeRevalidateTag } from 'lib/cache';
import { hash } from 'ohash';
import { UTApi } from 'uploadthing/server';
import { type z } from 'zod';
import { getUTApi } from '~/lib/uploadthing-server-helpers';
import { protocolInsertSchema } from '~/schemas/protocol';
import { requireApiAuth } from '~/utils/auth';
import { prisma } from '~/utils/db';
Expand Down Expand Up @@ -109,7 +109,7 @@ async function deleteFilesFromUploadThing(fileKey: string | string[]) {
return;
}

const utapi = new UTApi();
const utapi = await getUTApi();

const response = await utapi.deleteFiles(fileKey);

Expand Down
17 changes: 14 additions & 3 deletions actions/reset.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
'use server';

import { revalidatePath } from 'next/cache';
import { UTApi } from 'uploadthing/server';
import { env } from 'process';
import { safeRevalidateTag } from '~/lib/cache';
import { getUTApi } from '~/lib/uploadthing-server-helpers';
import { requireApiAuth } from '~/utils/auth';
import { prisma } from '~/utils/db';

export const resetAppSettings = async () => {
await requireApiAuth();
if (env.NODE_ENV !== 'development') {
await requireApiAuth();
}

try {
// Delete all data:
Expand All @@ -20,6 +23,14 @@ export const resetAppSettings = async () => {
prisma.asset.deleteMany(),
]);

// add a new initializedAt date
await prisma.appSettings.create({
data: {
key: 'initializedAt',
value: new Date().toISOString(),
},
});

revalidatePath('/');
safeRevalidateTag('appSettings');
safeRevalidateTag('activityFeed');
Expand All @@ -28,7 +39,7 @@ export const resetAppSettings = async () => {
safeRevalidateTag('getParticipants');
safeRevalidateTag('getInterviews');

const utapi = new UTApi();
const utapi = await getUTApi();

// Remove all files from UploadThing:
await utapi.listFiles({}).then(({ files }) => {
Expand Down
6 changes: 3 additions & 3 deletions actions/uploadThing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@

import { File } from 'node:buffer';
import { readFile, unlink } from 'node:fs/promises';
import { UTApi } from 'uploadthing/server';
import type {
ArchiveResult,
ExportReturn,
} from '~/lib/network-exporters/utils/types';
import { getUTApi } from '~/lib/uploadthing-server-helpers';
import { requireApiAuth } from '~/utils/auth';
import { ensureError } from '~/utils/ensureError';

export const deleteZipFromUploadThing = async (key: string) => {
await requireApiAuth();

const utapi = new UTApi();
const utapi = await getUTApi();

const deleteResponse = await utapi.deleteFiles(key);

Expand All @@ -34,7 +34,7 @@ export const uploadZipToUploadThing = async (
type: 'application/zip',
});

const utapi = new UTApi();
const utapi = await getUTApi();

const { data, error } = await utapi.uploadFiles(zipFile);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { submitUploadThingForm } from '~/actions/appSettings';
import Link from '~/components/Link';
import { Alert, AlertDescription, AlertTitle } from '~/components/ui/Alert';
import Heading from '~/components/ui/typography/Heading';
import Paragraph from '~/components/ui/typography/Paragraph';
import { UploadThingTokenForm } from '../UploadThingTokenForm';

function ConnectUploadThing() {
return (
<div className="w-[30rem]">
<div className="mb-4">
<Heading variant="h2">Connect UploadThing</Heading>
<Paragraph>
Fresco uses a third-party service called UploadThing to store media
files, including protocol assets. In order to use this service, you
need to create an account with UploadThing that will allow you to
generate a token that Fresco can use to securely communicate with it.
</Paragraph>
<Paragraph>
<Link
href="https://uploadthing.com/dashboard/new"
target="_blank"
rel="noopener noreferrer"
>
Click here
</Link>{' '}
to visit UploadThing. Create an app and copy and paste your API key
below.
</Paragraph>
<Alert variant="info" className="mt-4">
<AlertTitle>Good to know:</AlertTitle>
<AlertDescription>
Your UploadThing account is unique to you, meaning that no one else
will have access to the files stored in your instance of Fresco. For
more information about UploadThing, please review the{' '}
<Link href="https://docs.uploadthing.com/" target="_blank">
UploadThing Docs
</Link>
.
</AlertDescription>
</Alert>
<Paragraph>
For help, please refer to the{' '}
<Link
href="https://documentation.networkcanvas.com/en/fresco/deployment/guide#create-a-storage-bucket-using-uploadthing"
target="_blank"
rel="noopener noreferrer"
>
deployment guide
</Link>{' '}
in the Fresco documentation.
</Paragraph>
<UploadThingTokenForm action={submitUploadThingForm} />
</div>
</div>
);
}

export default ConnectUploadThing;
22 changes: 16 additions & 6 deletions app/(blobs)/(setup)/_components/OnboardSteps/Documentation.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { createId } from '@paralleldrive/cuid2';
import { FileText } from 'lucide-react';
import { setAppConfigured } from '~/actions/appSettings';
import { redirect } from 'next/navigation';
import { setAppSetting } from '~/actions/appSettings';
import Section from '~/components/layout/Section';
import { Button } from '~/components/ui/Button';
import SubmitButton from '~/components/ui/SubmitButton';
import Heading from '~/components/ui/typography/Heading';
import Paragraph from '~/components/ui/typography/Paragraph';
import trackEvent from '~/lib/analytics';
import { getInstallationId } from '~/queries/appSettings';

function Documentation() {
const handleAppConfigured = async () => {
await setAppConfigured();
const installationId = await getInstallationId();
if (!installationId) {
await setAppSetting('installationId', createId());
}
await setAppSetting('configured', true);
void trackEvent({
type: 'AppSetup',
metadata: {
installationId,
},
});

redirect('/dashboard');
};

return (
Expand Down Expand Up @@ -65,11 +77,9 @@ function Documentation() {
</Section>
</div>

<div className="flex justify-start pt-12">
<div className="flex justify-end pt-12">
<form action={handleAppConfigured}>
<SubmitButton variant="default" size={'lg'}>
Go to the dashboard!
</SubmitButton>
<SubmitButton variant="default">Go to the dashboard!</SubmitButton>
</form>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import ImportCSVModal from '~/app/dashboard/participants/_components/ImportCSVModal';
import Heading from '~/components/ui/typography/Heading';
import Paragraph from '~/components/ui/typography/Paragraph';
import AnonymousRecruitmentSwitchClient from '~/components/AnonymousRecruitmentSwitchClient';
import SettingsSection from '~/components/layout/SettingsSection';
import LimitInterviewsSwitchClient from '~/components/LimitInterviewsSwitchClient';
import Heading from '~/components/ui/typography/Heading';
import Paragraph from '~/components/ui/typography/Paragraph';
import OnboardContinue from '../OnboardContinue';
import AnonymousRecruitmentSwitchClient from '~/components/AnonymousRecruitmentSwitchClient';

function ManageParticipants({
allowAnonymousRecruitment,
Expand Down Expand Up @@ -55,7 +55,7 @@ function ManageParticipants({
</Paragraph>
</SettingsSection>
</div>
<div className="flex justify-start">
<div className="flex justify-end">
<OnboardContinue />
</div>
</div>
Expand Down
Loading

0 comments on commit 9f563c6

Please sign in to comment.