diff --git a/.env.defaults b/.env.defaults
index bc5fb1d..175fb7d 100644
--- a/.env.defaults
+++ b/.env.defaults
@@ -8,3 +8,7 @@ DATABASE_URL=file:./dev.db
# disables Prisma CLI update notifier
PRISMA_HIDE_UPDATE_MESSAGE=true
+
+CLOUDINARY_API_KEY=476712943135152
+# ask Kurt for help getting set up with a secret
+# CLOUDINARY_API_SECRET=
diff --git a/api/package.json b/api/package.json
index 844989e..cf3e712 100644
--- a/api/package.json
+++ b/api/package.json
@@ -3,6 +3,7 @@
"version": "0.0.0",
"private": true,
"dependencies": {
- "@redwoodjs/api": "^0.20.0"
+ "@redwoodjs/api": "^0.20.0",
+ "cloudinary": "^1.23.0"
}
}
diff --git a/api/prisma/migrations/20201227195638-add-subject-access-request-table/README.md b/api/prisma/migrations/20201227195638-add-subject-access-request-table/README.md
new file mode 100644
index 0000000..75d3f2d
--- /dev/null
+++ b/api/prisma/migrations/20201227195638-add-subject-access-request-table/README.md
@@ -0,0 +1,71 @@
+# Migration `20201227195638-add-subject-access-request-table`
+
+This migration has been generated by Kurt Hutten at 12/28/2020, 6:56:38 AM.
+You can check out the [state of the schema](./schema.prisma) after the migration.
+
+## Database Steps
+
+```sql
+CREATE TABLE "SubjectAccessRequest" (
+ "id" TEXT NOT NULL,
+ "comment" TEXT NOT NULL,
+ "payload" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+
+ FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE,
+PRIMARY KEY ("id")
+)
+```
+
+## Changes
+
+```diff
+diff --git schema.prisma schema.prisma
+migration 20201213004819-add-delete-on-part..20201227195638-add-subject-access-request-table
+--- datamodel.dml
++++ datamodel.dml
+@@ -1,7 +1,7 @@
+ datasource DS {
+ provider = ["sqlite", "postgresql"]
+- url = "***"
++ url = "***"
+ }
+ generator client {
+ provider = "prisma-client-js"
+@@ -30,13 +30,14 @@
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+- image String? // url maybe id or file storage service? cloudinary?
+- bio String? //mark down
+- Part Part[]
+- Reaction PartReaction[]
+- Comment Comment[]
++ image String? // url maybe id or file storage service? cloudinary?
++ bio String? //mark down
++ Part Part[]
++ Reaction PartReaction[]
++ Comment Comment[]
++ SubjectAccessRequest SubjectAccessRequest[]
+ }
+ model Part {
+ id String @id @default(uuid())
+@@ -78,4 +79,15 @@
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ }
++
++model SubjectAccessRequest {
++ id String @id @default(uuid())
++ comment String
++ payload String // json dump
++ user User @relation(fields: [userId], references: [id])
++ userId String
++
++ createdAt DateTime @default(now())
++ updatedAt DateTime @updatedAt
++}
+```
+
+
diff --git a/api/prisma/migrations/20201227195638-add-subject-access-request-table/schema.prisma b/api/prisma/migrations/20201227195638-add-subject-access-request-table/schema.prisma
new file mode 100644
index 0000000..c64ba45
--- /dev/null
+++ b/api/prisma/migrations/20201227195638-add-subject-access-request-table/schema.prisma
@@ -0,0 +1,93 @@
+datasource DS {
+ provider = ["sqlite", "postgresql"]
+ url = "***"
+}
+
+generator client {
+ provider = "prisma-client-js"
+ binaryTargets = ["native", "rhel-openssl-1.0.x"]
+}
+
+// sqlLight does not suport enums so we can't use enums until we set up postgresql in dev mode
+// enum Role {
+// USER
+// ADMIN
+// }
+
+// enum PartType {
+// CASCADESTUDIO
+// JSCAD
+// }
+
+model User {
+ id String @id @default(uuid())
+ userName String @unique // reffered to as userId in @relations
+ email String @unique
+ name String?
+ // role should probably be a list [] and also use enums, neither are supported by sqllight, so we need to set up postgresql in dev
+ // maybe let netlify handle roles for now.
+ // role String @default("user")
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ image String? // url maybe id or file storage service? cloudinary?
+ bio String? //mark down
+ Part Part[]
+ Reaction PartReaction[]
+ Comment Comment[]
+ SubjectAccessRequest SubjectAccessRequest[]
+}
+
+model Part {
+ id String @id @default(uuid())
+ title String
+ description String? // markdown string
+ code String?
+ mainImage String? // link to cloudinary
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ user User @relation(fields: [userId], references: [id])
+ userId String
+ deleted Boolean @default(false)
+
+ Comment Comment[]
+ Reaction PartReaction[]
+ @@unique([title, userId])
+}
+
+model PartReaction {
+ id String @id @default(uuid())
+ emote String // an emoji
+ user User @relation(fields: [userId], references: [id])
+ userId String
+ part Part @relation(fields: [partId], references: [id])
+ partId String
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ @@unique([emote, userId, partId])
+}
+
+model Comment {
+ id String @id @default(uuid())
+ text String // the comment, should I allow mark down?
+ user User @relation(fields: [userId], references: [id])
+ userId String
+ part Part @relation(fields: [partId], references: [id])
+ partId String
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+
+model SubjectAccessRequest {
+ id String @id @default(uuid())
+ comment String
+ payload String // json dump
+ user User @relation(fields: [userId], references: [id])
+ userId String
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
diff --git a/api/prisma/migrations/20201227195638-add-subject-access-request-table/steps.json b/api/prisma/migrations/20201227195638-add-subject-access-request-table/steps.json
new file mode 100644
index 0000000..d0ee8a8
--- /dev/null
+++ b/api/prisma/migrations/20201227195638-add-subject-access-request-table/steps.json
@@ -0,0 +1,176 @@
+{
+ "version": "0.3.14-fixed",
+ "steps": [
+ {
+ "tag": "CreateModel",
+ "model": "SubjectAccessRequest"
+ },
+ {
+ "tag": "CreateField",
+ "model": "SubjectAccessRequest",
+ "field": "id",
+ "type": "String",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateDirective",
+ "location": {
+ "path": {
+ "tag": "Field",
+ "model": "SubjectAccessRequest",
+ "field": "id"
+ },
+ "directive": "id"
+ }
+ },
+ {
+ "tag": "CreateDirective",
+ "location": {
+ "path": {
+ "tag": "Field",
+ "model": "SubjectAccessRequest",
+ "field": "id"
+ },
+ "directive": "default"
+ }
+ },
+ {
+ "tag": "CreateArgument",
+ "location": {
+ "tag": "Directive",
+ "path": {
+ "tag": "Field",
+ "model": "SubjectAccessRequest",
+ "field": "id"
+ },
+ "directive": "default"
+ },
+ "argument": "",
+ "value": "uuid()"
+ },
+ {
+ "tag": "CreateField",
+ "model": "SubjectAccessRequest",
+ "field": "comment",
+ "type": "String",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateField",
+ "model": "SubjectAccessRequest",
+ "field": "payload",
+ "type": "String",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateField",
+ "model": "SubjectAccessRequest",
+ "field": "user",
+ "type": "User",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateDirective",
+ "location": {
+ "path": {
+ "tag": "Field",
+ "model": "SubjectAccessRequest",
+ "field": "user"
+ },
+ "directive": "relation"
+ }
+ },
+ {
+ "tag": "CreateArgument",
+ "location": {
+ "tag": "Directive",
+ "path": {
+ "tag": "Field",
+ "model": "SubjectAccessRequest",
+ "field": "user"
+ },
+ "directive": "relation"
+ },
+ "argument": "fields",
+ "value": "[userId]"
+ },
+ {
+ "tag": "CreateArgument",
+ "location": {
+ "tag": "Directive",
+ "path": {
+ "tag": "Field",
+ "model": "SubjectAccessRequest",
+ "field": "user"
+ },
+ "directive": "relation"
+ },
+ "argument": "references",
+ "value": "[id]"
+ },
+ {
+ "tag": "CreateField",
+ "model": "SubjectAccessRequest",
+ "field": "userId",
+ "type": "String",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateField",
+ "model": "SubjectAccessRequest",
+ "field": "createdAt",
+ "type": "DateTime",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateDirective",
+ "location": {
+ "path": {
+ "tag": "Field",
+ "model": "SubjectAccessRequest",
+ "field": "createdAt"
+ },
+ "directive": "default"
+ }
+ },
+ {
+ "tag": "CreateArgument",
+ "location": {
+ "tag": "Directive",
+ "path": {
+ "tag": "Field",
+ "model": "SubjectAccessRequest",
+ "field": "createdAt"
+ },
+ "directive": "default"
+ },
+ "argument": "",
+ "value": "now()"
+ },
+ {
+ "tag": "CreateField",
+ "model": "SubjectAccessRequest",
+ "field": "updatedAt",
+ "type": "DateTime",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateDirective",
+ "location": {
+ "path": {
+ "tag": "Field",
+ "model": "SubjectAccessRequest",
+ "field": "updatedAt"
+ },
+ "directive": "updatedAt"
+ }
+ },
+ {
+ "tag": "CreateField",
+ "model": "User",
+ "field": "SubjectAccessRequest",
+ "type": "SubjectAccessRequest",
+ "arity": "List"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/api/prisma/migrations/migrate.lock b/api/prisma/migrations/migrate.lock
index d4f2e8e..2996315 100644
--- a/api/prisma/migrations/migrate.lock
+++ b/api/prisma/migrations/migrate.lock
@@ -2,4 +2,5 @@
20201101183848-db-init
20201105184423-add-name-to-user
-20201213004819-add-delete-on-part
\ No newline at end of file
+20201213004819-add-delete-on-part
+20201227195638-add-subject-access-request-table
\ No newline at end of file
diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma
index 13f644a..8a82a49 100644
--- a/api/prisma/schema.prisma
+++ b/api/prisma/schema.prisma
@@ -31,11 +31,12 @@ model User {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- image String? // url maybe id or file storage service? cloudinary?
- bio String? //mark down
- Part Part[]
- Reaction PartReaction[]
- Comment Comment[]
+ image String? // url maybe id or file storage service? cloudinary?
+ bio String? //mark down
+ Part Part[]
+ Reaction PartReaction[]
+ Comment Comment[]
+ SubjectAccessRequest SubjectAccessRequest[]
}
model Part {
@@ -79,3 +80,14 @@ model Comment {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
+
+model SubjectAccessRequest {
+ id String @id @default(uuid())
+ comment String
+ payload String // json dump
+ user User @relation(fields: [userId], references: [id])
+ userId String
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
diff --git a/api/src/graphql/subjectAccessRequests.sdl.js b/api/src/graphql/subjectAccessRequests.sdl.js
new file mode 100644
index 0000000..579b008
--- /dev/null
+++ b/api/src/graphql/subjectAccessRequests.sdl.js
@@ -0,0 +1,39 @@
+export const schema = gql`
+ type SubjectAccessRequest {
+ id: String!
+ comment: String!
+ payload: String!
+ user: User!
+ userId: String!
+ createdAt: DateTime!
+ updatedAt: DateTime!
+ }
+
+ type Query {
+ subjectAccessRequests: [SubjectAccessRequest!]!
+ subjectAccessRequest(id: String!): SubjectAccessRequest
+ }
+
+ input CreateSubjectAccessRequestInput {
+ comment: String!
+ payload: String!
+ userId: String!
+ }
+
+ input UpdateSubjectAccessRequestInput {
+ comment: String
+ payload: String
+ userId: String
+ }
+
+ type Mutation {
+ createSubjectAccessRequest(
+ input: CreateSubjectAccessRequestInput!
+ ): SubjectAccessRequest!
+ updateSubjectAccessRequest(
+ id: String!
+ input: UpdateSubjectAccessRequestInput!
+ ): SubjectAccessRequest!
+ deleteSubjectAccessRequest(id: String!): SubjectAccessRequest!
+ }
+`
diff --git a/api/src/graphql/users.sdl.js b/api/src/graphql/users.sdl.js
index 1830193..5365ba2 100644
--- a/api/src/graphql/users.sdl.js
+++ b/api/src/graphql/users.sdl.js
@@ -12,6 +12,7 @@ export const schema = gql`
Part(partTitle: String): Part
Reaction: [PartReaction]!
Comment: [Comment]!
+ SubjectAccessRequest: [SubjectAccessRequest]!
}
type Query {
diff --git a/api/src/services/helpers.js b/api/src/services/helpers.js
index 8a488bf..75faa62 100644
--- a/api/src/services/helpers.js
+++ b/api/src/services/helpers.js
@@ -1,3 +1,10 @@
+import { v2 as cloudinary } from 'cloudinary'
+cloudinary.config({
+ cloud_name: 'irevdev',
+ api_key: process.env.CLOUDINARY_API_KEY,
+ api_secret: process.env.CLOUDINARY_API_SECRET,
+})
+
export const foreignKeyReplacement = (input) => {
let output = input
const foreignKeys = Object.keys(input).filter((k) => k.match(/Id$/))
@@ -28,3 +35,14 @@ export const generateUniqueString = async (
const newSeed = count === 1 ? `${seed}_${count}` : seed.slice(0, -1) + count
return generateUniqueString(newSeed, isUniqueCallback, count)
}
+
+export const destroyImage = ({ publicId }) =>
+ new Promise((resolve, reject) => {
+ cloudinary.uploader.destroy(publicId, (error, result) => {
+ if (error) {
+ reject(error)
+ return
+ }
+ resolve(result)
+ })
+ })
diff --git a/api/src/services/parts/parts.js b/api/src/services/parts/parts.js
index d7b0df1..67385e2 100644
--- a/api/src/services/parts/parts.js
+++ b/api/src/services/parts/parts.js
@@ -3,6 +3,7 @@ import {
foreignKeyReplacement,
enforceAlphaNumeric,
generateUniqueString,
+ destroyImage,
} from 'src/services/helpers'
import { requireAuth } from 'src/lib/auth'
import { requireOwnership } from 'src/lib/owner'
@@ -74,10 +75,18 @@ export const updatePart = async ({ id, input }) => {
if (input.title) {
input.title = enforceAlphaNumeric(input.title)
}
- return db.part.update({
+ const originalPart = await db.part.findOne({ where: { id } })
+ const imageToDestroy =
+ originalPart.mainImage !== input.mainImage && originalPart.mainImage
+ const update = await db.part.update({
data: foreignKeyReplacement(input),
where: { id },
})
+ if (imageToDestroy) {
+ // destroy after the db has been updated
+ destroyImage({ publicId: imageToDestroy })
+ }
+ return update
}
export const deletePart = async ({ id }) => {
diff --git a/api/src/services/subjectAccessRequests/subjectAccessRequests.js b/api/src/services/subjectAccessRequests/subjectAccessRequests.js
new file mode 100644
index 0000000..8776520
--- /dev/null
+++ b/api/src/services/subjectAccessRequests/subjectAccessRequests.js
@@ -0,0 +1,42 @@
+import { db } from 'src/lib/db'
+import { requireAuth } from 'src/lib/auth'
+import { foreignKeyReplacement } from 'src/services/helpers'
+
+export const subjectAccessRequests = () => {
+ requireAuth({ role: 'admin' })
+ return db.subjectAccessRequest.findMany()
+}
+
+export const subjectAccessRequest = ({ id }) => {
+ requireAuth({ role: 'admin' })
+ return db.subjectAccessRequest.findOne({
+ where: { id },
+ })
+}
+
+export const createSubjectAccessRequest = ({ input }) => {
+ requireAuth({ role: 'admin' })
+ return db.subjectAccessRequest.create({
+ data: foreignKeyReplacement(input),
+ })
+}
+
+export const updateSubjectAccessRequest = ({ id, input }) => {
+ requireAuth({ role: 'admin' })
+ return db.subjectAccessRequest.update({
+ data: foreignKeyReplacement(input),
+ where: { id },
+ })
+}
+
+export const deleteSubjectAccessRequest = ({ id }) => {
+ requireAuth({ role: 'admin' })
+ return db.subjectAccessRequest.delete({
+ where: { id },
+ })
+}
+
+export const SubjectAccessRequest = {
+ user: (_obj, { root }) =>
+ db.subjectAccessRequest.findOne({ where: { id: root.id } }).user(),
+}
diff --git a/api/src/services/subjectAccessRequests/subjectAccessRequests.test.js b/api/src/services/subjectAccessRequests/subjectAccessRequests.test.js
new file mode 100644
index 0000000..88bcd4c
--- /dev/null
+++ b/api/src/services/subjectAccessRequests/subjectAccessRequests.test.js
@@ -0,0 +1,9 @@
+/*
+import { subjectAccessRequests } from './subjectAccessRequests'
+*/
+
+describe('subjectAccessRequests', () => {
+ it('returns true', () => {
+ expect(true).toBe(true)
+ })
+})
diff --git a/api/src/services/users/users.js b/api/src/services/users/users.js
index 7035f5a..2cb0586 100644
--- a/api/src/services/users/users.js
+++ b/api/src/services/users/users.js
@@ -2,7 +2,7 @@ import { db } from 'src/lib/db'
import { requireAuth } from 'src/lib/auth'
import { requireOwnership } from 'src/lib/owner'
import { UserInputError } from '@redwoodjs/api'
-import { enforceAlphaNumeric } from 'src/services/helpers'
+import { enforceAlphaNumeric, destroyImage } from 'src/services/helpers'
export const users = () => {
requireAuth({ role: 'admin' })
@@ -51,10 +51,18 @@ export const updateUserByUserName = async ({ userName, input }) => {
`You've tried to used a protected word as you userName, try something other than `
)
}
- return db.user.update({
+ const originalPart = await db.user.findOne({ where: { userName } })
+ const imageToDestroy =
+ originalPart.image !== input.image && originalPart.image
+ const update = await db.user.update({
data: input,
where: { userName },
})
+ if (imageToDestroy) {
+ // destroy after the db has been updated
+ destroyImage({ publicId: imageToDestroy })
+ }
+ return update
}
export const deleteUser = ({ id }) => {
@@ -80,4 +88,6 @@ export const User = {
db.user.findOne({ where: { id: root.id } }).Reaction(),
Comment: (_obj, { root }) =>
db.user.findOne({ where: { id: root.id } }).Comment(),
+ SubjectAccessRequest: (_obj, { root }) =>
+ db.user.findOne({ where: { id: root.id } }).SubjectAccessRequest(),
}
diff --git a/redwood.toml b/redwood.toml
index 40dc34e..cd93c42 100644
--- a/redwood.toml
+++ b/redwood.toml
@@ -8,7 +8,7 @@
[web]
port = 8910
apiProxyPath = "/.netlify/functions"
- includeEnvironmentVariables = ['GOOGLE_ANALYTICS_ID']
+ includeEnvironmentVariables = ['GOOGLE_ANALYTICS_ID', 'CLOUDINARY_API_KEY', 'CLOUDINARY_API_SECRET']
experimentalFastRefresh = true
[api]
port = 8911
diff --git a/web/src/Routes.js b/web/src/Routes.js
index e15d04d..eeadbf6 100644
--- a/web/src/Routes.js
+++ b/web/src/Routes.js
@@ -32,6 +32,7 @@ const Routes = () => {
)
return (