mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-23 19:29:17 +03:00
Authentication generation (#74)
This commit is contained in:
parent
724e8d5ac1
commit
6a59804a46
67
waspc/data/Generator/templates/react-app/src/api.js
vendored
Normal file
67
waspc/data/Generator/templates/react-app/src/api.js
vendored
Normal 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
|
22
waspc/data/Generator/templates/react-app/src/auth/login.js
vendored
Normal file
22
waspc/data/Generator/templates/react-app/src/auth/login.js
vendored
Normal 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
|
||||
|
17
waspc/data/Generator/templates/react-app/src/auth/logout.js
vendored
Normal file
17
waspc/data/Generator/templates/react-app/src/auth/logout.js
vendored
Normal 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
|
||||
|
24
waspc/data/Generator/templates/react-app/src/auth/useAuth.js
vendored
Normal file
24
waspc/data/Generator/templates/react-app/src/auth/useAuth.js
vendored
Normal 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
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
79
waspc/data/Generator/templates/server/src/core/auth.js
Normal file
79
waspc/data/Generator/templates/server/src/core/auth.js
Normal 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
|
||||
|
@ -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
|
||||
|
@ -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 })
|
||||
})
|
||||
|
10
waspc/data/Generator/templates/server/src/routes/auth/me.js
Normal file
10
waspc/data/Generator/templates/server/src/routes/auth/me.js
Normal 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()
|
||||
}
|
||||
})
|
@ -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
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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 =}
|
||||
|
@ -19,6 +19,7 @@ const Todo = (props) => {
|
||||
|
||||
const isThereAnyTask = () => tasks?.length > 0
|
||||
|
||||
|
||||
const createNewTask = async (description) => {
|
||||
const task = { isDone: false, description }
|
||||
await createTask(task)
|
||||
|
@ -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 } })
|
||||
|
||||
|
50
waspc/examples/todoApp/ext/pages/Login.js
Normal file
50
waspc/examples/todoApp/ext/pages/Login.js
Normal 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
|
@ -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
|
||||
|
60
waspc/examples/todoApp/ext/pages/Signup.js
Normal file
60
waspc/examples/todoApp/ext/pages/Signup.js
Normal 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
|
@ -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
|
||||
}
|
||||
|
@ -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 @@
|
@ -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
|
||||
```
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
+}
|
||||
+
|
||||
```
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
239
waspc/examples/todoApp/migrations/20201016123235-smth/steps.json
Normal file
239
waspc/examples/todoApp/migrations/20201016123235-smth/steps.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
# Prisma Migrate lockfile v1
|
||||
|
||||
20201001135152-init
|
||||
20201016123235-smth
|
@ -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]
|
||||
|
@ -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]
|
||||
|
64
waspc/src/Generator/ServerGenerator/AuthG.hs
Normal file
64
waspc/src/Generator/ServerGenerator/AuthG.hs
Normal 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)
|
||||
]
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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|])
|
||||
|
57
waspc/src/Generator/WebAppGenerator/AuthG.hs
Normal file
57
waspc/src/Generator/WebAppGenerator/AuthG.hs
Normal 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")
|
||||
-}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user