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
6 changes: 4 additions & 2 deletions .github/workflows/db-migrate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ on:
push:
branches:
- main
- develop
paths:
- 'src/lib/server/db/migrations/**'
workflow_dispatch:
jobs:
migrate:
runs-on: ubuntu-latest
environment: production
environment: ${{ github.ref == 'refs/heads/main' && 'production' || github.ref == 'refs/heads/develop' && 'preview' || 'preview' }}


steps:
- uses: actions/checkout@v4
Expand All @@ -32,4 +34,4 @@ jobs:
- name: Run database migrations
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: pnpm db:prod:migrate --force
run: pnpm db:prod:migrate
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"db:seed": "tsx src/lib/server/db/seed.ts",
"db:studio": "dotenv -- drizzle-kit studio",
"db:reset": "dotenv -- drizzle-kit drop && drizzle-kit push && pnpm db:seed",
"db:prod:migrate": "drizzle-kit push",
"db:prod:migrate": "drizzle-kit migrate",
"storybook": "storybook dev -p 6006 --no-open",
"build-storybook": "storybook build -o storybook-static",
"parse-openfoodfacts": "node src/lib/server/food-api/data/parse.js",
Expand Down
4 changes: 3 additions & 1 deletion src/lib/components/auth-form/GoogleButton.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<script lang="ts">
import GoogleIcon from '$lib/components/icons/GoogleIcon.svelte'
let { returnTo }: { returnTo?: string } = $props()
const href = $derived(`/login/google${returnTo ? `?returnTo=${encodeURIComponent(returnTo)}` : ''}`)
</script>

<a href="/login/google" class="google-button">
<a href={href} class="google-button">
<button type="button" class="google-button-inner">
<GoogleIcon />
Sign in with Google
Expand Down
10 changes: 7 additions & 3 deletions src/lib/components/login-popup/LoginPopup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@

let {
isOpen = $bindable(false),
onClose
onClose,
returnTo,
message
}: {
isOpen?: boolean
onClose?: () => void
returnTo?: string
message?: string
} = $props()
</script>

<Popup bind:isOpen width="400px" {onClose}>
<div class="login-popup-content">
<div class="message">
<p>Please log in to access this feature.</p>
<p>{message ?? 'Please log in to access this feature.'}</p>
</div>

<div class="button-group">
<GoogleButton />
<GoogleButton {returnTo} />
</div>
</div>
</Popup>
Expand Down
75 changes: 47 additions & 28 deletions src/lib/pages/new-recipe/NewRecipe.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
} from '$lib/utils/ingredient-formatting'
import DownloadIcon from 'lucide-svelte/icons/download'
import ImportRecipePopup from '$lib/components/recipe-scraper/ImportRecipePopup.svelte'
import AnonUploadPopup from '$lib/components/recipe-scraper/AnonUploadPopup.svelte'
import LoginPopup from '$lib/components/login-popup/LoginPopup.svelte'
import type { ImportedRecipeData } from '../../../../scripts/import-recipe-worker'
import FormError from '$lib/components/form-error/FormError.svelte'
Expand Down Expand Up @@ -155,15 +154,14 @@
let isMobileView = $state(false)
let searchValue = $state('')
let isImportPopupOpen = $state(false)
let isAnonUploadPopupOpen = $state(false)
let isLoginPopupOpen = $state(false)
let willUploadAnonymously = $state(false)
let estimatingNutrition = $state(false)
let displayNutrition = $state(false)
let previewRecipeData = $state<any>()
let confirmUpload = $state(false)
let imageUrlState = $state<string | undefined>(prefilledData?.image ?? undefined)
let showPreview = $state(false)
let loginPopupMessage = $state<string | undefined>()

const buildPreviewData = () => {
const previewRecipe = {
Expand Down Expand Up @@ -590,6 +588,39 @@
submitting = false
}
}

function persistFormToSession() {
try {
const data: ImportedRecipeData = {
title: _title,
description: _description,
servings: servings,
tags: selectedTags,
image: imageUrlState ?? null as any,
nutritionMode: displayNutrition ? 'manual' : 'none',
nutrition: displayNutrition
? {
calories: parseFloat(String(calories)) || 0,
protein: parseFloat(String(protein)) || 0,
carbs: parseFloat(String(carbs)) || 0,
fat: parseFloat(String(fat)) || 0
}
: undefined,
instructions: instructions.map((instruction) => ({
text: instruction.text,
mediaUrl: instruction.mediaUrl ?? null as any,
mediaType: instruction.mediaType ?? null as any,
ingredients: instruction.ingredients.map((ingredient) => ({
name: ingredient.name,
quantity: ingredient.isPrepared ? '' : (ingredient.quantity ?? ''),
measurement: ingredient.isPrepared ? '' : (ingredient.unit ?? ''),
isPrepared: ingredient.isPrepared === true
}))
}))
}
sessionStorage.setItem('forkly_prefilled_recipe', JSON.stringify(data))
} catch {}
}
</script>

{#snippet title()}
Expand Down Expand Up @@ -878,27 +909,15 @@

{#snippet previewButton(fullWidth?: boolean)}
<Button
disabled={!isLoggedIn}
{fullWidth}
loading={submitting}
type="button"
color="secondary"
onclick={async () => {
const loggedIn = await isLoggedIn

if (!loggedIn && !willUploadAnonymously && !editMode && !draftMode) {
isAnonUploadPopupOpen = true
return
}
if (!confirmUpload) {
if (!validateForm()) return
previewRecipeData = buildPreviewData()
showPreview = true
onOpenPreview?.()
return
}
confirmUpload = false
await submitCreate()
if (!validateForm()) return
previewRecipeData = buildPreviewData()
showPreview = true
onOpenPreview?.()
}}
>
Preview Recipe
Expand Down Expand Up @@ -1019,6 +1038,14 @@
<Button
color="primary"
onclick={async () => {
const loggedIn = await isLoggedIn
if (!loggedIn) {
persistFormToSession()
showPreview = false
loginPopupMessage = 'Please log in to upload the recipe.'
isLoginPopupOpen = true
return
}
showPreview = false
if (!editMode) {
await new Promise((resolve) => setTimeout(resolve, 300))
Expand Down Expand Up @@ -1105,15 +1132,7 @@
</div>
{/if}

<AnonUploadPopup
bind:isOpen={isAnonUploadPopupOpen}
onClose={() => (isAnonUploadPopupOpen = false)}
onUploadAnonymously={() => {
willUploadAnonymously = true
}}
/>

<LoginPopup bind:isOpen={isLoginPopupOpen} onClose={() => (isLoginPopupOpen = false)} />
<LoginPopup bind:isOpen={isLoginPopupOpen} onClose={() => { isLoginPopupOpen = false; loginPopupMessage = undefined }} returnTo="/new" message={loginPopupMessage} />
</div>

{#if !editMode}
Expand Down
6 changes: 4 additions & 2 deletions src/lib/remote-functions/recipe.remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const transformIngredientQuantity = (

export const createRecipe = command(RecipeSchema, async (input) => {
const { locals } = getRequestEvent()
if (!locals.user) error(401, 'Unauthorized')
await moveMediaFromTmpFolder(input)

ensureRecipeHasIngredients(input)
Expand All @@ -72,7 +73,7 @@ export const createRecipe = command(RecipeSchema, async (input) => {
let newRecipe: Awaited<ReturnType<typeof createRecipeDb>>

try {
newRecipe = await createRecipeDb(transformedInput, locals.user?.id)
newRecipe = await createRecipeDb(transformedInput, locals.user.id)
} catch (e) {
console.error('Error creating recipe', e)
await cleanupUploadedMediaFromObject(input)
Expand All @@ -95,6 +96,7 @@ const UpdateRecipeSchema = v.intersect([

export const updateRecipe = command(UpdateRecipeSchema, async (input) => {
const { locals } = getRequestEvent()
if (!locals.user) error(401, 'Unauthorized')
await moveMediaFromTmpFolder(input)

ensureRecipeHasIngredients(input)
Expand All @@ -110,7 +112,7 @@ export const updateRecipe = command(UpdateRecipeSchema, async (input) => {
let updatedRecipe: Awaited<ReturnType<typeof updateRecipeDb>>

try {
updatedRecipe = await updateRecipeDb(transformedInput, locals.user?.id)
updatedRecipe = await updateRecipeDb(transformedInput, locals.user.id)
} catch (e) {
console.error('Error updating recipe', e)
await cleanupUploadedMediaFromObject(input)
Expand Down
2 changes: 1 addition & 1 deletion src/lib/server/db/migrations/develop_test.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
This is a test file to trigger the database migration workflow on the develop branch.
Created for testing CI/CD pipeline integration.
test2dawdawdawdawd
8 changes: 3 additions & 5 deletions src/lib/server/db/recipe-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ async function insertInstructionsAndIngredients(tx: PostgresJsDatabase, recipeId
}
}

export async function createRecipe(input: RecipeInput, userId?: string) {
export async function createRecipe(input: RecipeInput, userId: string) {
return await db.transaction(async (tx) => {
const recipeId = generateId()

Expand All @@ -121,11 +121,9 @@ export async function createRecipe(input: RecipeInput, userId?: string) {
})
}

export async function updateRecipe(input: RecipeInput & { id: string }, userId?: string) {
export async function updateRecipe(input: RecipeInput & { id: string }, userId: string) {
return await db.transaction(async (tx) => {
const whereExpr = userId
? and(eq(recipe.id, input.id), eq(recipe.userId, userId))
: eq(recipe.id, input.id)
const whereExpr = and(eq(recipe.id, input.id), eq(recipe.userId, userId))

const updated = await tx.update(recipe).set({
title: input.title,
Expand Down
1 change: 1 addition & 0 deletions src/lib/server/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const ingredient = pgTable('ingredient', {
export const recipe = pgTable('recipe', {
id: text('id').primaryKey(),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
description: text('description'),
Expand Down
34 changes: 32 additions & 2 deletions src/lib/server/db/seed.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { recipe, ingredient, recipeInstruction, recipeIngredient, recipeNutrition, tag, recipeTag } from './schema'
import { recipe, ingredient, recipeInstruction, recipeIngredient, recipeNutrition, tag, recipeTag, user } from './schema'
import type { Recipe } from './schema'
import { generateId } from '../id'
import postgres from 'postgres'
Expand All @@ -7,6 +7,8 @@ import * as dotenv from 'dotenv'
import { readFile } from 'fs/promises'
import { normalizeIngredientName } from '../utils/normalize-ingredient'
import { parseQuantityToNumber } from '../../utils/ingredient-formatting'
import { hash } from '@node-rs/argon2'
import { eq } from 'drizzle-orm'

dotenv.config()

Expand All @@ -28,6 +30,34 @@ export const seed = async () => {
console.log('Error cleaning up data:', error)
}

// Ensure a demo user exists and capture the owner user id
const demoEmail = 'demo@forkly.local'
const demoUsername = 'demo'
const demoPasswordHash = await hash('password', {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1
})
const demoUserId = generateId()

await db
.insert(user)
.values({
id: demoUserId,
username: demoUsername,
passwordHash: demoPasswordHash,
email: demoEmail,
emailVerified: true
})
.onConflictDoUpdate({
target: user.email,
set: { username: demoUsername, passwordHash: demoPasswordHash, emailVerified: true }
})

const [owner] = await db.select({ id: user.id }).from(user).where(eq(user.email, demoEmail))
const ownerUserId = owner?.id ?? demoUserId

// Load recipes from JSON
const recipesJson = await readFile(new URL('./seedRecipes.json', import.meta.url), 'utf-8')
const sampleRecipesRaw: Array<Omit<Recipe, 'id' | 'createdAt'>> = JSON.parse(recipesJson)
Expand All @@ -52,6 +82,7 @@ export const seed = async () => {
...recipe,
title: newTitle,
id: generateId(),
userId: ownerUserId,
createdAt: new Date()
})
if (sampleRecipes.length === TARGET_RECIPE_COUNT) break
Expand Down Expand Up @@ -167,7 +198,6 @@ export const seed = async () => {
return Math.floor(Math.random() * (max - min + 1)) + min
}
function randomNutrition(recipe: Recipe) {
// Optionally, you could use recipe.tags or title to bias the ranges
return {
recipeId: recipe.id,
calories: randomInt(150, 900),
Expand Down
2 changes: 1 addition & 1 deletion src/routes/(api)/avatar/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ export const POST: RequestHandler = async ({ request, locals }) => {
await updateUserProfile(locals.user.id, { avatarUrl })

return json({ avatarUrl })
}
}
10 changes: 10 additions & 0 deletions src/routes/(pages)/(auth-form)/login/google/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ export const GET: RequestHandler = async (event) => {
const codeVerifier = generateCodeVerifier()
const url = google.createAuthorizationURL(state, codeVerifier, ["openid", "profile", "email"])

const returnTo = event.url.searchParams.get('returnTo')
if (returnTo) {
event.cookies.set('post_login_redirect', returnTo, {
path: '/',
httpOnly: true,
maxAge: 60 * 10,
sameSite: 'lax'
})
}

event.cookies.set("google_oauth_state", state, {
path: "/",
httpOnly: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,13 @@ export const GET: RequestHandler = async (event) => {
const session = await createSession(sessionToken, user.id)
setSessionTokenCookie(event, sessionToken, session.expiresAt)

const redirectTo = event.cookies.get('post_login_redirect') || '/'
event.cookies.delete('post_login_redirect', { path: '/' })

return new Response(null, {
status: 302,
headers: {
Location: "/"
Location: redirectTo
}
})
}