diff --git a/.env b/.env index 5b30dbb..eec3b83 100644 --- a/.env +++ b/.env @@ -1,11 +1,7 @@ - #NODE_TLS_REJECT_UNAUTHORIZED='0' -SSL_ENABLED=false -NEXT_PUBLIC_PROTOCOL=https -NEXT_PUBLIC_HOST=localhost -NEXT_PUBLIC_PORT=3003 -NEXTAUTH_URL=https://localhost:3003 -# NEXTAUTH_URL_INTERNAL=http://127.0.0.1:3003 +# HOST=localhost +# PORT=3003 +# NEXT_PUBLIC_PUBLIC_URL=http://localhost:3003 # Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58 diff --git a/.env.demo b/.env.demo deleted file mode 100644 index 17f98ce..0000000 --- a/.env.demo +++ /dev/null @@ -1,9 +0,0 @@ -NEXT_PUBLIC_PROTOCOL=https -NEXT_PUBLIC_PORT= -NEXT_PUBLIC_HOST=staging.mwhitnessing.com -NEXTAUTH_URL= https://staging.mwhitnessing.com - -# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 -NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638 -# ? do we need to duplicate this? already defined in the deoployment yml file -DATABASE_URL=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb:3306/jwpwsofia_demo \ No newline at end of file diff --git a/.env.development b/.env.development index afec269..21fcc42 100644 --- a/.env.development +++ b/.env.development @@ -1,15 +1,12 @@ NODE_TLS_REJECT_UNAUTHORIZED=0 # NODE_EXTRA_CA_CERTS=C:\\Users\\popov\\AppData\\Local\\mkcert +PROTOCOL=https +PORT=3003 +HOST=localhost +NEXT_PUBLIC_PUBLIC_URL=https://localhost:3003 -NEXT_PUBLIC_PROTOCOL=https -NEXT_PUBLIC_HOST=localhost -NEXT_PUBLIC_PORT=3003 -NEXTAUTH_URL=https://localhost:3003 - -SSL_ENABLED=true -TELEGRAM_BOT=true SSL_KEY=./certificates/localhost-key.pem SSL_CERT=./certificates/localhost.pem DATABASE_URL=mysql://root:Zelen0ku4e@192.168.0.10:3306/cart_dev -# DATABASE_URL=mysql://cart:cartpw@localhost:3306/cart +# DATABASE_URL=mysql://cart:cartpw@localhost:3306/cart \ No newline at end of file diff --git a/.env.homelab b/.env.homelab deleted file mode 100644 index 2a2aa9a..0000000 --- a/.env.homelab +++ /dev/null @@ -1,37 +0,0 @@ -NODE_TLS_REJECT_UNAUTHORIZED='0' -# DATABASE_URL="file:./src/data/dev.db" -# DATABASE_URL="mysql://root:Zelen0ku4e@192.168.0.10:3306/cart" - -NEXT_PUBLIC_PORT= -# NEXT_PUBLIC_NEXTAUTH_URL=https://cart.d-popov.com -NEXT_PUBLIC_PROTOCOL=https -NEXT_PUBLIC_HOST=cart.d-popov.com -NEXTAUTH_URL=https://cart.d-popov.com -# NEXTAUTH_URL= https://demo.mwhitnessing.com - -# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 -NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58 -DATABASE_URL=mysql://cart:cart2023@192.168.0.10:3306/cart_demo - -APPLE_ID= -APPLE_TEAM_ID= -APPLE_PRIVATE_KEY= -APPLE_KEY_ID= - -AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK -AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x -AUTH0_ISSUER=https://dev-wkzi658ckibr1amv.us.auth0.com - -FACEBOOK_ID= -FACEBOOK_SECRET= - -GITHUB_ID= -GITHUB_SECRET= -# GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com -# GOOGLE_SECRET=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 - -TWITTER_ID= -TWITTER_SECRET= - -EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525 -EMAIL_FROM=noreply@example.com diff --git a/.env.production b/.env.production index a3438f2..5853ac7 100644 --- a/.env.production +++ b/.env.production @@ -1,7 +1,7 @@ -NEXT_PUBLIC_PROTOCOL=https -NEXT_PUBLIC_PORT= -NEXT_PUBLIC_HOST=sofia.mwhitnessing.com -NEXTAUTH_URL= https://sofia.mwhitnessing.com +PORT= +HOST=sofia.mwitnessing.com +PROTOCOL=http # we're behind a reverse proxy. SSL is handled by the proxy +NEXT_PUBLIC_PUBLIC_URL= https://sofia.mwitnessing.com # Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638 diff --git a/.env.test b/.env.test index 1b9eef8..2e62731 100644 --- a/.env.test +++ b/.env.test @@ -1,13 +1,12 @@ -NODE_TLS_REJECT_UNAUTHORIZED='0' - -NEXT_PUBLIC_PORT=5001 -NEXT_PUBLIC_PROTOCOL=https -NEXT_PUBLIC_HOST=cart.d-popov.com -NEXTAUTH_URL=https://cart.d-popov.com +PROTOCOL=http +HOST=staging.mwitnessing.com +PORT= +NEXT_PUBLIC_PUBLIC_URL=https://staging.mwitnessing.com # Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 -NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58 -DATABASE_URL=mysql://cart:cartpw@192.168.0.10:3306/cart_dev +NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638 +# ? do we need to duplicate this? already defined in the deoployment yml file +DATABASE_URL=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb:3306/jwpwsofia_demo APPLE_ID= APPLE_TEAM_ID= @@ -23,14 +22,11 @@ FACEBOOK_SECRET= GITHUB_ID= GITHUB_SECRET= -GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com -GOOGLE_SECRET=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 +# GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com +# GOOGLE_SECRET=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 TWITTER_ID= TWITTER_SECRET= EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525 EMAIL_FROM=noreply@example.com - -GMAIL_EMAIL_USERNAME= -GMAIL_EMAIL_APP_PASS= diff --git a/.vscode/launch.json b/.vscode/launch.json index b11332d..963bb15 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "configurations": [ { "name": "Run npm nodemon (DEV)", - "command": "npm run debug-env", + "command": "npm run debug", "request": "launch", "type": "node-terminal", "preLaunchTask": "killInspector", @@ -35,7 +35,7 @@ "request": "launch", "type": "node-terminal", "cwd": "${workspaceFolder}", - "command": "conda activate node && npm run debug-env", + "command": "conda activate node && npm run debug", }, { "name": "Run conda npm TEST", diff --git a/_deploy/appleKey.p8 b/_deploy/appleKey.p8 new file mode 100644 index 0000000..4c686a8 --- /dev/null +++ b/_deploy/appleKey.p8 @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgL3WoWMr7zzqtZdF/ +wNEJ9+yMP2qNJV305gTdF+++hLOgCgYIKoZIzj0DAQehRANCAATqlUN+GE7/r8UQ +c93hRG9UxCtBcJEcgSGwYVPtZvA5igUBxY/6+RO/Tcnq9xT/6PZD0A82vMNSjoJ6 +/KyhaFLl +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/_deploy/appleKey_modified.p8 b/_deploy/appleKey_modified.p8 new file mode 100644 index 0000000..e9ed0ed --- /dev/null +++ b/_deploy/appleKey_modified.p8 @@ -0,0 +1 @@ +-----BEGIN PRIVATE KEY----- MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgL3WoWMr7zzqtZdF/ wNEJ9+yMP2qNJV305gTdF+++hLOgCgYIKoZIzj0DAQehRANCAATqlUN+GE7/r8UQ c93hRG9UxCtBcJEcgSGwYVPtZvA5igUBxY/6+RO/Tcnq9xT/6PZD0A82vMNSjoJ6 /KyhaFLl -----END PRIVATE KEY----- diff --git a/_deploy/deoloy.azure.demo.yml b/_deploy/deoloy.azure.demo.yml index 924858a..d81bbbb 100644 --- a/_deploy/deoloy.azure.demo.yml +++ b/_deploy/deoloy.azure.demo.yml @@ -1,6 +1,6 @@ version: "3" services: - nextjs-app: # https://sofia.mwhitnessing.com/ + nextjs-app: # https://sofia.mwitnessing.com/ hostname: jwpw-app-staging # jwpw-nextjs-app-1 image: docker.d-popov.com/jwpw:latest volumes: diff --git a/_deploy/deoloy.azure.production.yml b/_deploy/deoloy.azure.production.yml index 1ebca74..d653294 100644 --- a/_deploy/deoloy.azure.production.yml +++ b/_deploy/deoloy.azure.production.yml @@ -1,6 +1,6 @@ version: "3" services: - nextjs-app: # https://sofia.mwhitnessing.com/ + nextjs-app: # https://sofia.mwitnessing.com/ hostname: jwpw-app # jwpw-nextjs-app-1 image: docker.d-popov.com/jwpw:latest deploy: diff --git a/_deploy/entrypoint.sh b/_deploy/entrypoint.sh index 8a7d603..f0ea095 100644 --- a/_deploy/entrypoint.sh +++ b/_deploy/entrypoint.sh @@ -10,7 +10,7 @@ if [ "$UPDATE_CODE_FROM_GIT" = "true" ]; then mkdir /tmp/clone # Clone the repository - git clone -b ${GIT_BRANCH:-main} --depth 1 https://$GIT_USERNAME:${GIT_PASSWORD//@/%40}@git.d-popov.com/popov/mwhitnessing.git /tmp/clone || exit 1 + git clone -b ${GIT_BRANCH:-main} --depth 1 https://$GIT_USERNAME:${GIT_PASSWORD//@/%40}@git.d-popov.com/popov/mwitnessing.git /tmp/clone || exit 1 # Synchronize all files except package.json and package-lock.json to /app rsync -av --delete --exclude 'package.json' --exclude 'package-lock.json' /tmp/clone/ /app/ || echo "Rsync failed: Issue synchronizing files" diff --git a/_deploy/setupAppleId.mjs b/_deploy/setupAppleId.mjs new file mode 100644 index 0000000..00e85a0 --- /dev/null +++ b/_deploy/setupAppleId.mjs @@ -0,0 +1,74 @@ +#!/bin/node + +import { SignJWT } from "jose" +import { createPrivateKey } from "crypto" + +if (process.argv.includes("--help") || process.argv.includes("-h")) { + console.log(` + Creates a JWT from the components found at Apple. + By default, the JWT has a 6 months expiry date. + Read more: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048 + + Usage: + node apple.mjs [--kid] [--iss] [--private_key] [--sub] [--expires_in] [--exp] + APPLE_ID=com.mwhitnessing.sofia +APPLE_TEAM_ID=XC57P9SXDK +APPLE_KEY_ID=TB3V355G5Y +APPLE_KEY=-----BEGIN PRIVATE KEY----- MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgL3WoWMr7zzqtZdF/wNEJ9+yMP2qNJV305gTdF+++hLOgCgYIKoZIzj0DAQehRANCAATqlUN+GE7/r8UQc93hRG9UxCtBcJEcgSGwYVPtZvA5igUBxY/6+RO/Tcnq9xT/6PZD0A82vMNSjoJ6/KyhaFLl -----END PRIVATE KEY----- +node setupAppleId.mjs --kid YOUR_KEY_ID --iss YOUR_TEAM_ID --private_key "$(cat key.p8)" --sub YOUR_CLIENT_ID --expires_in 15778800 + + node setupAppleId.mjs --kid TB3V355G5Y --iss XC57P9SXDK --sub com.mwhitnessing.sofia --private_key "-----BEGIN PRIVATE KEY----- MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgL3WoWMr7zzqtZdF/ wNEJ9+yMP2qNJV305gTdF+++hLOgCgYIKoZIzj0DAQehRANCAATqlUN+GE7/r8UQ c93hRG9UxCtBcJEcgSGwYVPtZvA5igUBxY/6+RO/Tcnq9xT/6PZD0A82vMNSjoJ6 /KyhaFLl -----END PRIVATE KEY-----" + + + Options: + --help Print this help message + --kid, --key_id The key id of the private key + --iss, --team_id The Apple team ID + --private_key The private key to use to sign the JWT. (Starts with -----BEGIN PRIVATE KEY-----) + --sub, --client_id The client id to use in the JWT. + --expires_in Number of seconds from now when the JWT should expire. Defaults to 6 months. + --exp Future date in seconds when the JWT expires + `) +} else { + const args = process.argv.slice(2).reduce((acc, arg, i) => { + if (arg.match(/^--\w/)) { + const key = arg.replace(/^--/, "").toLowerCase() + acc[key] = process.argv[i + 3] + } + return acc + }, {}) + + const { + team_id, + iss = team_id, + + private_key, + + client_id, + sub = client_id, + + key_id, + kid = key_id, + + expires_in = 86400 * 180, + exp = Math.ceil(Date.now() / 1000) + expires_in, + } = args + + /** + * How long is the secret valid in seconds. + * @default 15780000 + */ + const expiresAt = Math.ceil(Date.now() / 1000) + expires_in + const expirationTime = exp ?? expiresAt + console.log(` +Apple client secret generated. Valid until: ${new Date(expirationTime * 1000)} + +${await new SignJWT({}) + .setAudience("https://appleid.apple.com") + .setIssuer(iss) + .setIssuedAt() + .setExpirationTime(expirationTime) + .setSubject(sub) + .setProtectedHeader({ alg: "ES256", kid }) + .sign(createPrivateKey(private_key.replace(/\\n/g, "\n")))}`) +} \ No newline at end of file diff --git a/_doc/ToDo.md b/_doc/ToDo.md index ae6c5fd..bd32c11 100644 --- a/_doc/ToDo.md +++ b/_doc/ToDo.md @@ -187,3 +187,12 @@ fix availability repeat checks sometimes delete from mycalendar fails saturday shifts start at 12:00 / dymamic +------------------------------- +Add availability type UNAVAILABLE/ AWAY (like Estelle, Rick, Me) + +why "Александра Чернъшова" seems available every shift thursdays? +fix Time ZONE (currently Z, but it leads to shift when the DST changes ( winter entries are shifter in summer)) +защо Марсел Клайнер е червен четв 11 април? - има предпочитания и е в номата +fix repeating availabilities - Tanq kolcjanova only blue first thursday +add assignment in calendar planner +fix database diff --git a/components/ExampleForm.js b/components/ExampleForm.js index 45f49f8..bf9050f 100644 --- a/components/ExampleForm.js +++ b/components/ExampleForm.js @@ -40,7 +40,7 @@ class ExampleForm extends React.Component { } const [item, set] = useState({ - isactive: true, + isActive: true, }); const router = useRouter(); @@ -63,7 +63,7 @@ class ExampleForm extends React.Component { handleChange = ({ target }) => { - if (target.name === "isactive") { + if (target.name === "isActive") { set({ ...item, [target.name]: target.checked }); } else if (target.name === "age") { set({ ...item, [target.name]: parseInt(target.value) }); @@ -100,8 +100,8 @@ class ExampleForm extends React.Component {

{router.query?.id ? "Редактирай" : "Създай"} Item

- -
diff --git a/components/availability/AvailabilityForm.js b/components/availability/AvailabilityForm.js index f95870e..a24f2af 100644 --- a/components/availability/AvailabilityForm.js +++ b/components/availability/AvailabilityForm.js @@ -1,5 +1,5 @@ import axiosInstance from '../../src/axiosSecure'; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, use } from "react"; import toast from "react-hot-toast"; import { useRouter } from "next/router"; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; @@ -26,9 +26,6 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o const [editMode, setEditMode] = useState(existingItems.length > 0); const [publisher, setPublisher] = useState({ id: publisherId }); const [day, setDay] = useState(new Date(date)); - const [doRepeat, setDoRepeat] = useState(false); - const [repeatFrequency, setRepeatFrequency] = useState(1); - const [repeatUntil, setRepeatUntil] = useState(null); const [canUpdate, setCanUpdate] = useState(true); const [timeSlots, setTimeSlots] = useState([]); @@ -39,13 +36,17 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o dayOfMonth: null, // startTime: "08:00", // endTime: "20:00", - isactive: true, + isActive: true, repeatWeekly: false, endDate: null, isFirst: false, isLast: false, }]); + const [doRepeat, setDoRepeat] = useState(existingItems && existingItems.length > 0 ? existingItems[0].repeatWeekly : false); + const [repeatFrequency, setRepeatFrequency] = useState(1); + const [repeatUntil, setRepeatUntil] = useState(null); + const [isInline, setInline] = useState(inline || false); const [config, setConfig] = useState(null); useEffect(() => { @@ -69,6 +70,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o const response = await axiosInstance.get(`/api/data/availabilities/${id}`); setAvailabilities([response.data]); setEditMode(true); + setDoRepeat(response.data.repeatWeekly); } catch (error) { console.error(error); toast.error("Error fetching availability data."); @@ -85,27 +87,29 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o e.preventDefault(); try { const groupedTimeSlots = mergeCheckedTimeSlots(timeSlots); - + let avs = availabilities.filter(av => av.type !== "assignment"); // Determine if we need to delete and recreate, or just update - const shouldRecreate = availabilities.length !== groupedTimeSlots.length || availabilities.some(av => !av.id); - + let shouldRecreate = avs.length > 0 && avs.length !== groupedTimeSlots.length || avs.some(av => !av.id); + shouldRecreate = shouldRecreate || ( avs.length == 0 && availabilities.length > 0); + //create availability if we open a form with assignment without availability + if (shouldRecreate) { // Delete existing availabilities if they have an ID console.log("Recreating availabilities"); - await Promise.all(availabilities.filter(av => av.id).map(av => axiosInstance.delete(`${urls.apiUrl}${av.id}`))); + await Promise.all(avs.filter(av => av.id).map(av => axiosInstance.delete(`${urls.apiUrl}${av.id}`))); // Create new availabilities - const createdAvailabilities = await Promise.all(groupedTimeSlots.map(async group => { + avs = await Promise.all(groupedTimeSlots.map(async group => { const newAvailability = createAvailabilityFromGroup(group, publisher.id); const response = await axiosInstance.post(urls.apiUrl, newAvailability); return response.data; // Assuming the new availability is returned })); - setAvailabilities(createdAvailabilities); + setAvailabilities(avs); } else { // Update existing availabilities console.log("Updating existing availabilities"); - const updatedAvailabilities = await Promise.all(availabilities.map(async (availability, index) => { + avs = await Promise.all(avs.map(async (availability, index) => { const group = groupedTimeSlots[index]; const id = availability.id; const updatedAvailability = updateAvailabilityFromGroup(availability, group); @@ -119,7 +123,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o return updatedAvailability; })); - setAvailabilities(updatedAvailabilities); + setAvailabilities(avs); } handleCompletion({ updated: true }); @@ -202,17 +206,32 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o availability.isWithTransportIn = group[0].isFirst && timeSlots[0].isWithTransport; availability.isWithTransportOut = group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport; - availability.repeatWeekly = doRepeat; - availability.dayOfMonth = doRepeat ? null : availability.startTime.getDate(); - availability.endDate = doRepeat ? repeatUntil : null; + delete availability.weekOfMonth; + if (doRepeat) { + availability.repeatWeekly = true; + availability.dayOfMonth = null; + availability.weekOfMonth = 0; + availability.endDate = repeatUntil; + } else { + availability.repeatWeekly = false; + availability.dayOfMonth = availability.startTime.getDate(); + availability.endDate = null; + } + availability.dateOfEntry = new Date(); + if (availability.parentAvailabilityId) { + availability.parentAvailability = { connect: { id: parentAvailabilityId } }; + } + delete availability.parentAvailabilityId; + return availability; } const handleDelete = async (e) => { e.preventDefault(); try { - const deletePromises = availabilities.map(async (availability) => { + let avs = availabilities.filter(av => av.type !== "assignment"); + const deletePromises = avs.map(async (availability) => { if (availability.id) { // console.log("deleting publisher id = ", router.query.id, "; url=" + urls.apiUrl + router.query.id); await axiosInstance.delete(urls.apiUrl + availability.id); @@ -288,8 +307,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o const TimeSlotCheckboxes = ({ slots, setSlots, items: [] }) => { - const [allDay, setAllDay] = useState(false); - + const [allDay, setAllDay] = useState(slots.every(slot => slot.isChecked)); const handleAllDayChange = (e) => { const updatedSlots = slots.map(slot => ({ ...slot, @@ -297,7 +315,9 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o })); setSlots(updatedSlots); setAllDay(e.target.checked) - setCanUpdate(true); + // setCanUpdate(slots.some(slot => slot.isChecked)); + const anyChecked = updatedSlots.some(slot => slot.isChecked); + setCanUpdate(anyChecked); console.log("handleAllDayChange: allDay: " + allDay + ", updatedSlots: " + JSON.stringify(updatedSlots)); }; useEffect(() => { @@ -352,9 +372,9 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o return (
-