Authentication generation (#74)

This commit is contained in:
Matija Sosic 2020-10-19 14:45:54 +02:00 committed by GitHub
parent 724e8d5ac1
commit 6a59804a46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1155 additions and 48 deletions

View File

@ -0,0 +1,67 @@
import axios from 'axios'
import config from './config'
const api = axios.create({
baseURL: config.apiUrl,
})
const WASP_APP_AUTH_TOKEN_NAME = "authToken"
let authToken = null
if (window.localStorage) {
authToken = window.localStorage.getItem(WASP_APP_AUTH_TOKEN_NAME)
}
export const setAuthToken = (token) => {
if (typeof token !== 'string') {
throw Error(`Token must be a string, but it was: {${typeof token}} ${token}.`)
}
authToken = token
window.localStorage && window.localStorage.setItem(WASP_APP_AUTH_TOKEN_NAME, token)
}
export const clearAuthToken = () => {
authToken = undefined
window.localStorage && window.localStorage.removeItem(WASP_APP_AUTH_TOKEN_NAME)
}
export const clearLocalStorage = () => {
authToken = undefined
window.localStorage && window.localStorage.clear()
}
api.interceptors.request.use(request => {
if (authToken) {
request.headers['Authorization'] = `Bearer ${authToken}`
}
return request
})
/**
* Takes an error returned by the app's API (as returned by axios), and transforms into a more
* standard format to be further used by the client. It is also assumed that given API
* error has been formatted as implemented by HttpError on the server.
*/
export const handleApiError = (error) => {
if (error?.response) {
// If error came from HTTP response, we capture most informative message
// and also add .statusCode information to it.
// If error had JSON response, we assume it is of format { message, data } and
// add that info to the error.
// TODO: We might want to use HttpError here instead of just Error, since
// HttpError is also used on server to throw errors like these.
// That would require copying HttpError code to web-app also and using it here.
const responseJson = error.response?.data
const responseStatusCode = error.response.status
const e = new Error(responseJson?.message || error.message)
e.statusCode = responseStatusCode
e.data = responseJson?.data
throw e
} else {
// If any other error, we just propagate it.
throw error
}
}
export default api

View File

@ -0,0 +1,22 @@
import config from '../config.js'
import queryCache from '../queryCache'
import api, { setAuthToken, handleApiError } from '../api.js'
const login = async (email, password) => {
try {
const args = { email, password }
const response = await api.post(config.apiUrl + '/auth/login', args)
setAuthToken(response.data.token)
// TODO(matija): Currently we are invalidating all the queries, but we should invalidate only
// non-public, user-dependent queries - public queries are expected not to change in respect
// to the currently logged in user.
queryCache.invalidateQueries()
} catch (error) {
handleApiError(error)
}
}
export default login

View File

@ -0,0 +1,17 @@
import { clearLocalStorage } from '../api.js'
import queryCache from '../queryCache'
const logout = () => {
clearLocalStorage()
// TODO(matija): We are currently invalidating all the queries, but we should invalidate only the
// non-public, user-dependent ones.
queryCache.invalidateQueries()
// TODO(matija): We are currently clearing all the queries, but we should clear only the
// non-public, user-dependent ones.
queryCache.clear()
}
export default logout

View File

@ -0,0 +1,24 @@
import { useQuery } from '../queries'
import config from '../config.js'
import api, { setAuthToken, handleApiError } from '../api.js'
const getMe = async () => {
try {
const response = await api.get(config.apiUrl + '/auth/me')
return response.data
} catch (error) {
if (error.response?.status === 403) {
return null
} else {
handleApiError(error)
}
}
}
getMe.queryCacheKey = 'auth/me'
const useAuth = (queryFnArgs, config) => {
return useQuery(getMe, queryFnArgs, config)
}
export default useAuth

View File

@ -1,30 +1,13 @@
{{={= =}=}}
import axios from 'axios'
import api, { handleApiError } from '../api.js'
import config from '../config.js'
export const callOperation = async (operationRoute, args) => {
try {
const response = await axios.post(config.apiUrl + '/' + operationRoute, args)
const response = await api.post(config.apiUrl + '/' + operationRoute, args)
return response.data
} catch (error) {
if (error?.response) {
// If error came from HTTP response, we capture most informative message
// and also add .statusCode information to it.
// If error had JSON response, we assume it is of format { message, data } and
// add that info to the error.
// TODO: We might want to use HttpError here instead of just Error, since
// HttpError is also used on server to throw errors like these.
// That would require copying HttpError code to web-app also and using it here.
const responseJson = error.response?.data
const responseStatusCode = error.response.status
const e = new Error(responseJson?.message || error.message)
e.statusCode = responseStatusCode
e.data = responseJson?.data
throw e
} else {
// If any other error, we just propagate it.
throw error
}
handleApiError(error)
}
}

View File

@ -0,0 +1,79 @@
{{={= =}=}}
import jwt from 'jsonwebtoken'
import SecurePassword from 'secure-password'
import util from 'util'
import Prisma from '@prisma/client'
import { handleRejection } from '../utils.js'
const prisma = new Prisma.PrismaClient()
const jwtSign = util.promisify(jwt.sign)
const jwtVerify = util.promisify(jwt.verify)
// TODO(matija): this is not safe, this value should come from some config file/environment
// and shouldn't be commited to the version control.
const JWT_SECRET = "developmentJwtSecret"
export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options)
export const verify = (token) => jwtVerify(token, JWT_SECRET)
const auth = handleRejection(async (req, res, next) => {
const authHeader = req.get('Authorization')
if (!authHeader) {
// NOTE(matija): for now we let tokenless requests through and make it operation's
// responsibility to verify whether the request is authenticated or not. In the future
// we will develop our own system at Wasp-level for that.
return next()
}
if (authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7, authHeader.length)
const userIdFromToken = (await verify(token)).id
const user = await prisma.{= userEntityLower =}.findOne({ where: { id: userIdFromToken } })
if (!user) {
return res.status(401).send()
}
const { password, ...userView } = user
req.user = userView
} else {
return res.status(401).send()
}
next()
})
export const createNewUser = async (userFields) => {
const hashedPassword = await hashPassword(userFields.password)
const newUser = await prisma.{= userEntityLower =}.create({
data: {
...userFields,
password: hashedPassword
},
})
return newUser
}
const SP = new SecurePassword()
export const hashPassword = async (password) => {
const hashedPwdBuffer = await SP.hash(Buffer.from(password))
return hashedPwdBuffer.toString("base64")
}
export const verifyPassword = async (hashedPassword, password) => {
try {
return await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
} catch (error) {
console.error(error)
return false
}
}
export default auth

View File

@ -0,0 +1,13 @@
import express from 'express'
import auth from '../../core/auth.js'
import login from './login.js'
import me from './me.js'
const router = express.Router()
router.post('/login', login)
router.get('/me', auth, me)
export default router

View File

@ -0,0 +1,41 @@
{{={= =}=}}
import Prisma from '@prisma/client'
import SecurePassword from 'secure-password'
import { sign, verifyPassword } from '../../core/auth.js'
import { handleRejection } from '../../utils.js'
const prisma = new Prisma.PrismaClient()
export default handleRejection(async (req, res) => {
const args = req.body || {}
const context = {}
// Try to fetch user with the given email.
const {= userEntityLower =} = await prisma.{= userEntityLower =}.findOne({ where: { email: args.email.toLowerCase() } })
if (!user) {
return res.status(401).send()
}
// We got user - now check the password.
const verifyPassRes = await verifyPassword({= userEntityLower =}.password, args.password)
switch (verifyPassRes) {
case SecurePassword.VALID:
break
case SecurePassword.VALID_NEEDS_REHASH:
// TODO(matija): take neccessary steps to make the password more secure.
break
default:
return res.status(401).send()
}
// Email & password valid - generate token.
const token = await sign({= userEntityLower =}.id)
// NOTE(matija): Possible option - instead of explicitly returning token here,
// we could add to response header 'Set-Cookie {token}' directive which would then make
// browser automatically save cookie with token.
return res.json({ token })
})

View File

@ -0,0 +1,10 @@
{{={= =}=}}
import { handleRejection } from '../../utils.js'
export default handleRejection(async (req, res) => {
if (req.{= userEntityLower =}) {
return res.json(req.{= userEntityLower =})
} else {
return res.status(403).send()
}
})

View File

@ -1,6 +1,8 @@
{{={= =}=}}
import express from 'express'
import operations from './operations/index.js'
import auth from './auth/index.js'
const router = express.Router()
@ -8,6 +10,7 @@ router.get('/', function (req, res, next) {
res.json('Hello world')
})
router.use('/auth', auth)
router.use('/{= operationsRouteInRootRouter =}', operations)
export default router

View File

@ -6,7 +6,12 @@ import {= operationName =} from "{= operationImportPath =}"
export default handleRejection(async (req, res) => {
const args = req.body || {}
const context = {}
const context = {
{=# userEntityLower =}
user: req.user
{=/ userEntityLower =}
}
const result = await {= operationName =}(args, context)
res.json(result)
})

View File

@ -12,7 +12,13 @@ export default handleRejection(async (req, res) => {
So for now we are just going with POST that has JSON in the body -> generated code is not
as human-like as it should be though. =}
const args = req.body || {}
const context = {}
const context = {
{=# userEntityLower =}
user: req.user
{=/ userEntityLower =}
}
const result = await {= operationName =}(args, context)
res.json(result)
})

View File

@ -1,12 +1,16 @@
{{={= =}=}}
import express from 'express'
import auth from '../../core/auth.js'
{=# operationRoutes =}
import {= importIdentifier =} from '{= importPath =}'
{=/ operationRoutes =}
const router = express.Router()
router.use(auth)
{=# operationRoutes =}
router.post('{= routePath =}', {= importIdentifier =})
{=/ operationRoutes =}

View File

@ -19,6 +19,7 @@ const Todo = (props) => {
const isThereAnyTask = () => tasks?.length > 0
const createNewTask = async (description) => {
const task = { isDone: false, description }
await createTask(task)

View File

@ -1,6 +1,15 @@
import HttpError from '@wasp/core/HttpError.js'
import { createNewUser } from '@wasp/core/auth.js'
export const signUp = async (args, context) => {
await createNewUser({ email: args.email, password: args.password })
}
export const createTask = async (task, context) => {
if (!context.user) {
throw new HttpError(403)
}
const Task = context.entities.Task
/*
if (Math.random() < 0.5) {
@ -16,6 +25,10 @@ export const createTask = async (task, context) => {
}
export const updateTaskIsDone = async ({ taskId, newIsDoneVal }, context) => {
if (!context.user) {
throw new HttpError(403)
}
const Task = context.entities.Task
return Task.update({
where: { id: taskId },
@ -24,6 +37,10 @@ export const updateTaskIsDone = async ({ taskId, newIsDoneVal }, context) => {
}
export const deleteCompletedTasks = async (args, context) => {
if (!context.user) {
throw new HttpError(403)
}
const Task = context.entities.Task
await Task.deleteMany({
where: { isDone: true }
@ -31,6 +48,10 @@ export const deleteCompletedTasks = async (args, context) => {
}
export const toggleAllTasks = async (args, context) => {
if (!context.user) {
throw new HttpError(403)
}
const Task = context.entities.Task
const notDoneTasksCount = await Task.count({ where: { isDone: false } })

View File

@ -0,0 +1,50 @@
import React, { useState } from 'react'
import { useHistory } from 'react-router-dom'
import login from '@wasp/auth/login.js'
const Login = (props) => {
const LoginForm = () => {
const history = useHistory()
const [emailFieldVal, setEmailFieldVal] = useState('')
const [passwordFieldVal, setPasswordFieldVal] = useState('')
const handleLogin = async (event) => {
event.preventDefault()
try {
await login(emailFieldVal, passwordFieldVal)
history.push('/')
} catch (err) {
console.log(err)
window.alert('Error:' + err.message)
}
}
return (
<form onSubmit={handleLogin}>
<h2>Email</h2>
<input
type="text"
value={emailFieldVal}
onChange={e => setEmailFieldVal(e.target.value)}
/>
<h2>Password</h2>
<input
type="password"
value={passwordFieldVal}
onChange={e => setPasswordFieldVal(e.target.value)}
/>
<div>
<input type="submit" value="Log in"/>
</div>
</form>
)
}
return <LoginForm/>
}
export default Login

View File

@ -1,17 +1,27 @@
import React, { Component } from 'react'
import React, { useState } from 'react'
import { Link } from "react-router-dom"
import useAuth from '@wasp/auth/useAuth.js'
import logout from '@wasp/auth/logout.js'
import Todo from "../Todo.js"
import '../Main.css'
export default class Main extends Component {
// TODO: Add propTypes.
const Main = () => {
const { data: user } = useAuth()
render() {
if (!user) {
return (
<>
<Todo/>
</>
<span>
Please <Link to="/login">login</Link> or <Link to="/signup">sign up</Link>.
</span>
)
} else {
return <>
<button onClick={logout}>Logout</button>
<Todo/>
</>
}
}
export default Main

View File

@ -0,0 +1,60 @@
import React, { useState } from 'react'
import { useHistory } from 'react-router-dom'
import signUp from '@wasp/actions/signUp.js'
import login from '@wasp/auth/login.js'
const Signup = (props) => {
const SignUpForm = () => {
const history = useHistory()
const [emailFieldVal, setEmailFieldVal] = useState('')
const [passwordFieldVal, setPasswordFieldVal] = useState('')
const handleSignup = async (event) => {
event.preventDefault()
try {
await signUp({ email: emailFieldVal, password: passwordFieldVal })
await login (emailFieldVal, passwordFieldVal)
setEmailFieldVal('')
setPasswordFieldVal('')
// Redirect to main page.
history.push('/')
} catch (err) {
console.log(err)
window.alert('Error:' + err.message)
}
}
return (
<form onSubmit={handleSignup}>
<h2>Email</h2>
<input
type="text"
value={emailFieldVal}
onChange={e => setEmailFieldVal(e.target.value)}
/>
<h2>Password</h2>
<input
type="password"
value={passwordFieldVal}
onChange={e => setPasswordFieldVal(e.target.value)}
/>
<div>
<input type="submit" value="Sign up"/>
</div>
</form>
)
}
return (
<>
<SignUpForm/>
</>
)
}
export default Signup

View File

@ -2,6 +2,11 @@ import HttpError from '@wasp/core/HttpError.js'
export const getTasks = async (args, context) => {
if (!context.user) {
throw new HttpError(403)
}
console.log('user who made the query: ', context.user)
const Task = context.entities.Task
/*
if (Math.random() < 0.5) {
@ -15,7 +20,12 @@ export const getTasks = async (args, context) => {
}
export const getTask = async ({ id }, context) => {
const task = await prisma.task.findOne({ where: { id } })
if (!context.user) {
throw new HttpError(403)
}
const Task = context.entities.Task
const task = await Task.findOne({ where: { id } })
return task
}

View File

@ -1,6 +1,6 @@
# Migration `20201001135152-init`
# Migration `20201008125434-init`
This migration has been generated by Martin Sosic at 10/1/2020, 3:51:52 PM.
This migration has been generated by Matija Sosic at 10/8/2020, 2:54:34 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
@ -22,7 +22,7 @@ CREATE TABLE "Task" (
```diff
diff --git schema.prisma schema.prisma
migration ..20201001135152-init
migration ..20201008125434-init
--- datamodel.dml
+++ datamodel.dml
@@ -1,0 +1,29 @@

View File

@ -0,0 +1,46 @@
# Migration `20201008135054-added-user`
This migration has been generated by Matija Sosic at 10/8/2020, 3:50:54 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL
)
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email")
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration 20201008125434-init..20201008135054-added-user
--- datamodel.dml
+++ datamodel.dml
@@ -1,15 +1,21 @@
datasource db {
provider = "sqlite"
- url = "***"
+ url = "***"
}
generator client {
provider = "prisma-client-js"
output = "../server/node_modules/.prisma/client"
}
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ password String
+}
+
model Project {
id Int @id @default(autoincrement())
name String
```

View File

@ -0,0 +1,35 @@
datasource db {
provider = "sqlite"
url = "***"
}
generator client {
provider = "prisma-client-js"
output = "../server/node_modules/.prisma/client"
}
model User {
id Int @id @default(autoincrement())
email String @unique
password String
}
model Project {
id Int @id @default(autoincrement())
name String
// NOTE(matija): not using relations yet.
//tasks Task[]
}
model Task {
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
// NOTE(matija): not using relations yet.
//project Project @relation(fields: [projectId], references: [id])
//projectId Int
}

View File

@ -0,0 +1,77 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateModel",
"model": "User"
},
{
"tag": "CreateField",
"model": "User",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateField",
"model": "User",
"field": "email",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "email"
},
"directive": "unique"
}
},
{
"tag": "CreateField",
"model": "User",
"field": "password",
"type": "String",
"arity": "Required"
}
]
}

View File

@ -0,0 +1,74 @@
# Migration `20201016123235-smth`
This migration has been generated by Matija Sosic at 10/16/2020, 2:32:35 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL
)
CREATE TABLE "Project" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL
)
CREATE TABLE "Task" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"description" TEXT NOT NULL,
"isDone" BOOLEAN NOT NULL DEFAULT false
)
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email")
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration ..20201016123235-smth
--- datamodel.dml
+++ datamodel.dml
@@ -1,0 +1,35 @@
+
+datasource db {
+ provider = "sqlite"
+ url = "***"
+}
+
+generator client {
+ provider = "prisma-client-js"
+ output = "../server/node_modules/.prisma/client"
+}
+
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ password String
+}
+
+model Project {
+ id Int @id @default(autoincrement())
+ name String
+
+ // NOTE(matija): not using relations yet.
+ //tasks Task[]
+}
+
+model Task {
+ id Int @id @default(autoincrement())
+ description String
+ isDone Boolean @default(false)
+
+ // NOTE(matija): not using relations yet.
+ //project Project @relation(fields: [projectId], references: [id])
+ //projectId Int
+}
+
```

View File

@ -0,0 +1,35 @@
datasource db {
provider = "sqlite"
url = "***"
}
generator client {
provider = "prisma-client-js"
output = "../server/node_modules/.prisma/client"
}
model User {
id Int @id @default(autoincrement())
email String @unique
password String
}
model Project {
id Int @id @default(autoincrement())
name String
// NOTE(matija): not using relations yet.
//tasks Task[]
}
model Task {
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
// NOTE(matija): not using relations yet.
//project Project @relation(fields: [projectId], references: [id])
//projectId Int
}

View File

@ -0,0 +1,239 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateSource",
"source": "db"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Source",
"source": "db"
},
"argument": "provider",
"value": "\"sqlite\""
},
{
"tag": "CreateArgument",
"location": {
"tag": "Source",
"source": "db"
},
"argument": "url",
"value": "\"***\""
},
{
"tag": "CreateModel",
"model": "User"
},
{
"tag": "CreateField",
"model": "User",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateField",
"model": "User",
"field": "email",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "email"
},
"directive": "unique"
}
},
{
"tag": "CreateField",
"model": "User",
"field": "password",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateModel",
"model": "Project"
},
{
"tag": "CreateField",
"model": "Project",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Project",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Project",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Project",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateField",
"model": "Project",
"field": "name",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateModel",
"model": "Task"
},
{
"tag": "CreateField",
"model": "Task",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Task",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Task",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Task",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateField",
"model": "Task",
"field": "description",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Task",
"field": "isDone",
"type": "Boolean",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Task",
"field": "isDone"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Task",
"field": "isDone"
},
"directive": "default"
},
"argument": "",
"value": "false"
}
]
}

View File

@ -1,3 +1,3 @@
# Prisma Migrate lockfile v1
20201001135152-init
20201016123235-smth

View File

@ -2,6 +2,17 @@ app todoApp {
title: "ToDo App"
}
auth {
userEntity: User,
methods: [ EmailAndPassword ]
}
entityPSL User {=psl
id Int @id @default(autoincrement())
email String @unique
password String
psl=}
entityPSL Project {=psl
id Int @id @default(autoincrement())
name String
@ -20,6 +31,16 @@ entityPSL Task {=psl
//projectId Int
psl=}
route "/login" -> page Login
page Login {
component: import Login from "@ext/pages/Login"
}
route "/signup" -> page Signup
page Signup {
component: import Signup from "@ext/pages/Signup"
}
route "/" -> page Main
page Main {
component: import Main from "@ext/pages/Main"
@ -50,11 +71,17 @@ query getTasks {
}
query getTask {
fn: import { getTask } from "@ext/queries.js"
fn: import { getTask } from "@ext/queries.js",
entities: [Task]
}
// --------- Actions --------- //
action signUp {
fn: import { signUp } from "@ext/actions.js",
entities: [User]
}
action createTask {
fn: import { createTask } from "@ext/actions.js",
entities: [Task]

View File

@ -19,10 +19,8 @@ import qualified Generator.ServerGenerator.Common as C
import qualified Generator.ServerGenerator.ExternalCodeGenerator as ServerExternalCodeGenerator
import Generator.ServerGenerator.OperationsG (genOperations)
import Generator.ServerGenerator.OperationsRoutesG (genOperationsRoutes)
import Generator.ServerGenerator.AuthG (genAuth)
import qualified NpmDependency as ND
import StrongPath (File, Path,
Rel)
import qualified StrongPath as SP
import Wasp (Wasp)
import qualified Wasp
import qualified Wasp.NpmDependencies as WND
@ -68,6 +66,8 @@ waspNpmDeps = ND.fromList
, ("express", "~4.16.1")
, ("morgan", "~1.9.1")
, ("@prisma/client", "2.x")
, ("jsonwebtoken", "^8.5.1")
, ("secure-password", "^4.0.0")
]
-- TODO: Also extract devDependencies like we did dependencies (waspNpmDeps).
@ -87,18 +87,16 @@ genGitignore _ = C.makeTemplateFD (asTmplFile [P.relfile|gitignore|])
(asServerFile [P.relfile|.gitignore|])
Nothing
asTmplSrcFile :: P.Path P.Rel P.File -> Path (Rel C.ServerTemplatesSrcDir) File
asTmplSrcFile = SP.fromPathRelFile
genSrcDir :: Wasp -> [FileDraft]
genSrcDir wasp = concat
[ [C.copySrcTmplAsIs $ asTmplSrcFile [P.relfile|app.js|]]
, [C.copySrcTmplAsIs $ asTmplSrcFile [P.relfile|server.js|]]
, [C.copySrcTmplAsIs $ asTmplSrcFile [P.relfile|utils.js|]]
, [C.copySrcTmplAsIs $ asTmplSrcFile [P.relfile|core/HttpError.js|]]
[ [C.copySrcTmplAsIs $ C.asTmplSrcFile [P.relfile|app.js|]]
, [C.copySrcTmplAsIs $ C.asTmplSrcFile [P.relfile|server.js|]]
, [C.copySrcTmplAsIs $ C.asTmplSrcFile [P.relfile|utils.js|]]
, [C.copySrcTmplAsIs $ C.asTmplSrcFile [P.relfile|core/HttpError.js|]]
, genRoutesDir wasp
, genOperationsRoutes wasp
, genOperations wasp
, genAuth wasp
]
genRoutesDir :: Wasp -> [FileDraft]

View File

@ -0,0 +1,64 @@
module Generator.ServerGenerator.AuthG
( genAuth
) where
import qualified Path as P
import Data.Aeson (object, (.=))
import qualified Util
import Wasp (Wasp, getAuth)
import qualified Wasp.Auth
import Generator.FileDraft (FileDraft)
import qualified Generator.ServerGenerator.Common as C
import StrongPath ((</>))
genAuth :: Wasp -> [FileDraft]
genAuth wasp = case maybeAuth of
Just auth -> [ genCoreAuth auth
-- Auth routes
, genAuthRoutesIndex
, genLoginRoute auth
, genMeRoute auth
]
Nothing -> []
where
maybeAuth = getAuth wasp
-- | Generates core/auth file which contains auth middleware and createUser() function.
genCoreAuth :: Wasp.Auth.Auth -> FileDraft
genCoreAuth auth = C.makeTemplateFD tmplFile dstFile (Just tmplData)
where
coreAuthRelToSrc = [P.relfile|core/auth.js|]
tmplFile = C.asTmplFile $ [P.reldir|src|] P.</> coreAuthRelToSrc
dstFile = C.serverSrcDirInServerRootDir </> (C.asServerSrcFile coreAuthRelToSrc)
tmplData = let userEntity = (Wasp.Auth._userEntity auth) in object
[ "userEntityUpper" .= userEntity
, "userEntityLower" .= Util.toLowerFirst userEntity
]
genAuthRoutesIndex :: FileDraft
genAuthRoutesIndex = C.copySrcTmplAsIs (C.asTmplSrcFile [P.relfile|routes/auth/index.js|])
genLoginRoute :: Wasp.Auth.Auth -> FileDraft
genLoginRoute auth = C.makeTemplateFD tmplFile dstFile (Just tmplData)
where
loginRouteRelToSrc = [P.relfile|routes/auth/login.js|]
tmplFile = C.asTmplFile $ [P.reldir|src|] P.</> loginRouteRelToSrc
dstFile = C.serverSrcDirInServerRootDir </> (C.asServerSrcFile loginRouteRelToSrc)
tmplData = let userEntity = (Wasp.Auth._userEntity auth) in object
[ "userEntityUpper" .= userEntity
, "userEntityLower" .= Util.toLowerFirst userEntity
]
genMeRoute :: Wasp.Auth.Auth -> FileDraft
genMeRoute auth = C.makeTemplateFD tmplFile dstFile (Just tmplData)
where
meRouteRelToSrc = [P.relfile|routes/auth/me.js|]
tmplFile = C.asTmplFile $ [P.reldir|src|] P.</> meRouteRelToSrc
dstFile = C.serverSrcDirInServerRootDir </> (C.asServerSrcFile meRouteRelToSrc)
tmplData = object
[ "userEntityLower" .= Util.toLowerFirst (Wasp.Auth._userEntity auth)
]

View File

@ -8,7 +8,9 @@ module Generator.ServerGenerator.Common
, copySrcTmplAsIs
, srcDirInServerTemplatesDir
, asTmplFile
, asTmplSrcFile
, asServerFile
, asServerSrcFile
, ServerRootDir
, ServerSrcDir
, ServerTemplatesDir
@ -35,9 +37,14 @@ data ServerTemplatesSrcDir
asTmplFile :: P.Path P.Rel P.File -> Path (Rel ServerTemplatesDir) File
asTmplFile = SP.fromPathRelFile
asTmplSrcFile :: P.Path P.Rel P.File -> Path (Rel ServerTemplatesSrcDir) File
asTmplSrcFile = SP.fromPathRelFile
asServerFile :: P.Path P.Rel P.File -> Path (Rel ServerRootDir) File
asServerFile = SP.fromPathRelFile
asServerSrcFile :: P.Path P.Rel P.File -> Path (Rel ServerSrcDir) File
asServerSrcFile = SP.fromPathRelFile
-- * Paths

View File

@ -4,6 +4,7 @@ module Generator.ServerGenerator.OperationsRoutesG
) where
import Data.Aeson (object, (.=))
import qualified Data.Aeson as Aeson
import Data.Maybe (fromJust)
import qualified Path as P
import qualified System.FilePath.Posix as FPPosix
@ -20,6 +21,7 @@ import qualified Wasp
import qualified Wasp.Action
import qualified Wasp.Operation
import qualified Wasp.Query
import qualified Wasp.Auth
genOperationsRoutes :: Wasp -> [FileDraft]
@ -40,13 +42,21 @@ genQueryRoute wasp query = genOperationRoute wasp op tmplFile
tmplFile = C.asTmplFile [P.relfile|src/routes/operations/_query.js|]
genOperationRoute :: Wasp -> Wasp.Operation.Operation -> Path (Rel C.ServerTemplatesDir) File -> FileDraft
genOperationRoute _ operation tmplFile = C.makeTemplateFD tmplFile dstFile (Just tmplData)
genOperationRoute wasp operation tmplFile = C.makeTemplateFD tmplFile dstFile (Just tmplData)
where
dstFile = operationsRoutesDirInServerRootDir </> operationRouteFileInOperationsRoutesDir operation
tmplData = object
baseTmplData = object
[ "operationImportPath" .= operationImportPath
, "operationName" .= Wasp.Operation.getName operation
]
tmplData = case (Wasp.getAuth wasp) of
Nothing -> baseTmplData
Just auth -> U.jsonSet ("userEntityLower")
(Aeson.toJSON (U.toLowerFirst $ Wasp.Auth._userEntity auth))
baseTmplData
operationImportPath = relPosixPathFromOperationsRoutesDirToSrcDir
FPPosix.</> SP.toFilePath (SP.relFileToPosix' $ operationFileInSrcDir operation)

View File

@ -19,6 +19,7 @@ import qualified Generator.WebAppGenerator.EntityGenerator as EntityGenera
import qualified Generator.WebAppGenerator.ExternalCodeGenerator as WebAppExternalCodeGenerator
import Generator.WebAppGenerator.OperationsGenerator (genOperations)
import qualified Generator.WebAppGenerator.RouterGenerator as RouterGenerator
import qualified Generator.WebAppGenerator.AuthG as AuthG
import qualified NpmDependency as ND
import StrongPath (Dir, Path,
Rel, (</>))
@ -110,6 +111,7 @@ generateSrcDir wasp
++ EntityGenerator.generateEntities wasp
++ [generateReducersJs wasp]
++ genOperations wasp
++ AuthG.genAuth wasp
where
generateLogo = C.makeTemplateFD (asTmplFile [P.relfile|src/logo.png|])
(srcDir </> asWebAppSrcFile [P.relfile|logo.png|])

View File

@ -0,0 +1,57 @@
module Generator.WebAppGenerator.AuthG
( genAuth
) where
import qualified Path as P
import Wasp (Wasp, getAuth)
import Generator.FileDraft (FileDraft)
import Generator.WebAppGenerator.Common as C
genAuth :: Wasp -> [FileDraft]
genAuth wasp = case maybeAuth of
Just _ -> [ genApi
, genLogin
, genLogout
, genUseAuth
]
Nothing -> []
where
maybeAuth = getAuth wasp
-- | Generates api.js file which contains token management and configured api (e.g. axios) instance.
genApi :: FileDraft
genApi = C.copyTmplAsIs (C.asTmplFile [P.relfile|src/api.js|])
-- | Generates file with login function to be used by Wasp developer.
genLogin :: FileDraft
genLogin = C.copyTmplAsIs (C.asTmplFile [P.relfile|src/auth/login.js|])
-- | Generates file with logout function to be used by Wasp developer.
genLogout :: FileDraft
genLogout = C.copyTmplAsIs (C.asTmplFile [P.relfile|src/auth/logout.js|])
-- | Generates React hook that Wasp developer can use in a component to get
-- access to the currently logged in user (and check whether user is logged in
-- ot not).
genUseAuth :: FileDraft
genUseAuth = C.copyTmplAsIs (C.asTmplFile [P.relfile|src/auth/useAuth.js|])
{-
-- | Generates React hook that Wasp developer can use in a component to get
-- access to the currently logged in user (and check whether user is logged in
-- ot not).
genUseUser :: Wasp.Auth.Auth -> FileDraft
genUseUser auth = C.makeTemplateFD tmplFile dstFile (Just tmplData)
where
tmplFile = C.asTmplFile [P.relfile|src/auth/_useUser.js|]
dstFile = C.asWebAppFile $ [P.reldir|src/auth/|] P.</> fromJust (getUseUserDstFileName auth)
tmplData = object
[ "userEntityLower" .= Util.toLowerFirst (Wasp.Auth._userEntity auth)
, "userEntity" .= (Wasp.Auth._userEntity auth)
]
getUseUserDstFileName :: Wasp.Auth.Auth -> Maybe (P.Path P.Rel P.File)
getUseUserDstFileName a = P.parseRelFile ("use" ++ (Wasp.Auth._userEntity a) ++ ".js")
-}

View File

@ -12,6 +12,7 @@ module Wasp
, getApp
, setApp
, getAuth
, getPSLEntities
-- TODO(matija): Old Entity stuff, to be removed.
@ -133,6 +134,15 @@ setApp wasp app = wasp { waspElements = (WaspElementApp app) : (filter (not . is
fromApp :: App -> Wasp
fromApp app = fromWaspElems [WaspElementApp app]
-- * Auth
getAuth :: Wasp -> Maybe Wasp.Auth.Auth
getAuth wasp = let auths = [a | WaspElementAuth a <- waspElements wasp] in
case auths of
[] -> Nothing
[a] -> Just a
_ -> error "Wasp can't contain more than one WaspElementAuth element!"
-- * NpmDependencies
getNpmDependencies :: Wasp -> NpmDependencies