Add Privacy Policy related improvements

various thing to make sure we're GDPR, et al compliant
This commit is contained in:
Kurt Hutten
2020-12-25 17:29:01 +11:00
parent 6623939f78
commit 7d262e9f58
51 changed files with 1480 additions and 95 deletions

View File

@@ -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"
}
}

View File

@@ -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
+}
```

View File

@@ -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
}

View File

@@ -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"
}
]
}

View File

@@ -2,4 +2,5 @@
20201101183848-db-init
20201105184423-add-name-to-user
20201213004819-add-delete-on-part
20201213004819-add-delete-on-part
20201227195638-add-subject-access-request-table

View File

@@ -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
}

View File

@@ -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!
}
`

View File

@@ -12,6 +12,7 @@ export const schema = gql`
Part(partTitle: String): Part
Reaction: [PartReaction]!
Comment: [Comment]!
SubjectAccessRequest: [SubjectAccessRequest]!
}
type Query {

View File

@@ -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)
})
})

View File

@@ -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 }) => {

View File

@@ -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(),
}

View File

@@ -0,0 +1,9 @@
/*
import { subjectAccessRequests } from './subjectAccessRequests'
*/
describe('subjectAccessRequests', () => {
it('returns true', () => {
expect(true).toBe(true)
})
})

View File

@@ -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(),
}