Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zenstack latest WIP #2

Open
wants to merge 8 commits into
base: original
Choose a base branch
from
Open
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
16 changes: 2 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
# Redwood Tutorial App

This repo represents the final state of the app created during the [Redwood Tutorial](https://redwoodjs.com/tutorial).
It is meant to be a starting point for those working on the second half of the Tutorial, starting at the [Intermission](https://redwoodjs.com/docs/tutorial/intermission).
This project demonstrates how to use [ZenStack](https://zenstack.dev) in a RedwoodJS project. It's extended based on the blog app used through [RedwoodJS's tutorial](https://redwoodjs.com/docs/tutorial/foreword).

This repo contains much more styling than the one we built together in the tutorial, but is functionally identical.
Please refer to [this blog post](https://zenstack.dev/blog/redwood-auth) for a general introduction.

## Setup

The [tutorial itself](https://redwoodjs.com/docs/tutorial/chapter1/prerequisites) contains instructions for getting this repo up and running, but here is a summary of the commands:

```bash
git clone https://github.com/redwoodjs/redwood-tutorial
cd redwood-tutorial
yarn install
yarn rw prisma migrate dev
yarn rw dev
```
49 changes: 49 additions & 0 deletions api/db/migrations/20230620053259_add_policies/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT,
"email" TEXT NOT NULL,
"hashedPassword" TEXT NOT NULL,
"salt" TEXT NOT NULL,
"resetToken" TEXT,
"resetTokenExpiresAt" DATETIME,
"roles" TEXT NOT NULL DEFAULT 'moderator',
"zenstack_guard" BOOLEAN NOT NULL DEFAULT true,
"zenstack_transaction" TEXT
);
INSERT INTO "new_User" ("email", "hashedPassword", "id", "name", "resetToken", "resetTokenExpiresAt", "roles", "salt") SELECT "email", "hashedPassword", "id", "name", "resetToken", "resetTokenExpiresAt", "roles", "salt" FROM "User";
DROP TABLE "User";
ALTER TABLE "new_User" RENAME TO "User";
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
CREATE INDEX "User_zenstack_transaction_idx" ON "User"("zenstack_transaction");
CREATE TABLE "new_Comment" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"body" TEXT NOT NULL,
"postId" INTEGER NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"zenstack_guard" BOOLEAN NOT NULL DEFAULT true,
"zenstack_transaction" TEXT,
CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Comment" ("body", "createdAt", "id", "name", "postId") SELECT "body", "createdAt", "id", "name", "postId" FROM "Comment";
DROP TABLE "Comment";
ALTER TABLE "new_Comment" RENAME TO "Comment";
CREATE INDEX "Comment_zenstack_transaction_idx" ON "Comment"("zenstack_transaction");
CREATE TABLE "new_Post" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"zenstack_guard" BOOLEAN NOT NULL DEFAULT true,
"zenstack_transaction" TEXT,
CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Post" ("body", "createdAt", "id", "title", "userId") SELECT "body", "createdAt", "id", "title", "userId" FROM "Post";
DROP TABLE "Post";
ALTER TABLE "new_Post" RENAME TO "Post";
CREATE INDEX "Post_zenstack_transaction_idx" ON "Post"("zenstack_transaction");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;
19 changes: 19 additions & 0 deletions api/db/migrations/20230622084334_add_published/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Post" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"published" BOOLEAN NOT NULL DEFAULT true,
"zenstack_guard" BOOLEAN NOT NULL DEFAULT true,
"zenstack_transaction" TEXT,
CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Post" ("body", "createdAt", "id", "title", "userId", "zenstack_guard", "zenstack_transaction") SELECT "body", "createdAt", "id", "title", "userId", "zenstack_guard", "zenstack_transaction" FROM "Post";
DROP TABLE "Post";
ALTER TABLE "new_Post" RENAME TO "Post";
CREATE INDEX "Post_zenstack_transaction_idx" ON "Post"("zenstack_transaction");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;
90 changes: 58 additions & 32 deletions api/db/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,48 +1,74 @@
//////////////////////////////////////////////////////////////////////////////////////////////
// DO NOT MODIFY THIS FILE //
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
//////////////////////////////////////////////////////////////////////////////////////////////

datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
provider="sqlite"
url=env("DATABASE_URL")
}

generator client {
provider = "prisma-client-js"
binaryTargets = "native"
provider = "prisma-client-js"
binaryTargets = "native"
previewFeatures = ["interactiveTransactions"]
}

/// @@allow('all', auth().roles == 'admin' && auth() == user)
/// @@allow('read', published)
model Post {
id Int @id @default(autoincrement())
title String
body String
comments Comment[]
user User @relation(fields: [userId], references: [id])
userId Int
createdAt DateTime @default(now())
id Int @id() @default(autoincrement())
title String
body String
comments Comment[]
user User @relation(fields: [userId], references: [id])
userId Int
createdAt DateTime @default(now())
published Boolean @default(true)

zenstack_guard Boolean @default(true)
zenstack_transaction String?

@@index([zenstack_transaction])
}

model Contact {
id Int @id @default(autoincrement())
name String
email String
message String
createdAt DateTime @default(now())
id Int @id() @default(autoincrement())
name String
email String
message String
createdAt DateTime @default(now())
}

model User {
id Int @id @default(autoincrement())
name String?
email String @unique
hashedPassword String
salt String
resetToken String?
resetTokenExpiresAt DateTime?
roles String @default("moderator")
posts Post[]
id Int @id() @default(autoincrement())
name String?
email String @unique()
hashedPassword String
salt String
resetToken String?
resetTokenExpiresAt DateTime?
roles String @default("moderator")
posts Post[]

zenstack_guard Boolean @default(true)
zenstack_transaction String?

@@index([zenstack_transaction])
}

/// @@allow('all', auth().roles == 'moderator')
/// @@allow('create,read', post.published)
model Comment {
id Int @id @default(autoincrement())
name String
body String
post Post @relation(fields: [postId], references: [id])
postId Int
createdAt DateTime @default(now())
}
id Int @id() @default(autoincrement())
name String
body String
post Post @relation(fields: [postId], references: [id])
postId Int
createdAt DateTime @default(now())

zenstack_guard Boolean @default(true)
zenstack_transaction String?

@@index([zenstack_transaction])
}
6 changes: 5 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
"private": true,
"dependencies": {
"@redwoodjs/api": "3.2.0",
"@redwoodjs/graphql-server": "3.2.0"
"@redwoodjs/graphql-server": "3.2.0",
"@zenstackhq/runtime": "^1.0.0-beta.24"
},
"devDependencies": {
"zenstack": "^1.0.0-beta.24"
}
}
67 changes: 67 additions & 0 deletions api/schema.zmodel
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}

plugin prisma {
provider = '@core/prisma'
output = './db/schema.prisma'
}

generator client {
provider = "prisma-client-js"
binaryTargets = "native"
previewFeatures = ["interactiveTransactions"]
}

model Post {
id Int @id @default(autoincrement())
title String
body String
comments Comment[]
user User @relation(fields: [userId], references: [id])
userId Int
createdAt DateTime @default(now())
published Boolean @default(true)

// 🔐 Admin user can do everything to his own posts
@@allow('all', auth().roles == 'admin' && auth() == user)

// 🔐 Posts are visible to everyone if published
@@allow('read', published)
}

model Contact {
id Int @id @default(autoincrement())
name String
email String
message String
createdAt DateTime @default(now())
}

model User {
id Int @id @default(autoincrement())
name String?
email String @unique
hashedPassword String
salt String
resetToken String?
resetTokenExpiresAt DateTime?
roles String @default("moderator")
posts Post[]
}

model Comment {
id Int @id @default(autoincrement())
name String
body String
post Post @relation(fields: [postId], references: [id])
postId Int
createdAt DateTime @default(now())

// 🔐 Moderator user can do everything to comments
@@allow('all', auth().roles == 'moderator')

// 🔐 Everyone is allowed to view and create comments for published posts
@@allow('create,read', post.published)
}
2 changes: 2 additions & 0 deletions api/src/graphql/adminPosts.sdl.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const schema = gql`
createPost(input: CreatePostInput!): Post! @requireAuth(roles: ["admin"])
updatePost(id: Int!, input: UpdatePostInput!): Post!
@requireAuth(roles: ["admin"])
publishPost(id: Int!): Post! @requireAuth(roles: ["admin"])
unpublishPost(id: Int!): Post! @requireAuth(roles: ["admin"])
deletePost(id: Int!): Post! @requireAuth(roles: ["admin"])
}
`
2 changes: 1 addition & 1 deletion api/src/graphql/comments.sdl.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ export const schema = gql`

type Mutation {
createComment(input: CreateCommentInput!): Comment! @skipAuth
deleteComment(id: Int!): Comment! @requireAuth(roles: "moderator")
deleteComment(id: Int!): Comment! @skipAuth
}
`
1 change: 1 addition & 0 deletions api/src/graphql/posts.sdl.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const schema = gql`
body: String!
createdAt: DateTime!
user: User!
published: Boolean!
}

type Query {
Expand Down
8 changes: 8 additions & 0 deletions api/src/lib/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// for options.

import { PrismaClient } from '@prisma/client'
import { enhance } from '@zenstackhq/runtime'

import { emitLogLevels, handlePrismaLogging } from '@redwoodjs/api/logger'

Expand All @@ -14,6 +15,13 @@ export const db = new PrismaClient({
log: emitLogLevels(['info', 'warn', 'error']),
})

/*
* Returns ZenStack wrapped Prisma Client with access policies enabled.
*/
export function authDb() {
return enhance(db, { user: context.currentUser })
}

handlePrismaLogging({
db,
logger,
Expand Down
22 changes: 22 additions & 0 deletions api/src/services/adminPosts/adminPosts.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,28 @@ export const updatePost = async ({ id, input }) => {
})
}

export const publishPost = async ({ id }) => {
await validateOwnership({ id })

return db.post.update({
where: { id },
data: {
published: true,
},
})
}

export const unpublishPost = async ({ id }) => {
await validateOwnership({ id })

return db.post.update({
where: { id },
data: {
published: false,
},
})
}

export const deletePost = async ({ id }) => {
await validateOwnership({ id })

Expand Down
14 changes: 6 additions & 8 deletions api/src/services/comments/comments.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import { requireAuth } from 'src/lib/auth'
import { db } from 'src/lib/db'
import { authDb } from 'src/lib/db'

export const comments = ({ postId }) => {
return db.comment.findMany({ where: { postId } })
return authDb().comment.findMany({ where: { postId } })
}

export const comment = ({ id }) => {
return db.comment.findUnique({
return authDb().comment.findUnique({
where: { id },
})
}

export const createComment = ({ input }) => {
return db.comment.create({
return authDb().comment.create({
data: input,
})
}

export const deleteComment = ({ id }) => {
requireAuth({ roles: 'moderator' })
return db.comment.delete({
return authDb().comment.delete({
where: { id },
})
}

export const Comment = {
post: (_obj, { root }) => {
return db.comment.findUnique({ where: { id: root?.id } }).post()
return authDb().post.findUnique({ where: { id: root?.postId } })
},
}
Loading