Merge branch 'master' into realworld

This commit is contained in:
Martin Sosic 2020-12-01 11:54:57 +01:00
commit 6a4469c370
24 changed files with 302 additions and 245 deletions

View File

@ -1,63 +0,0 @@
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'
export default () => {
const [method, setMethod] = useState('login')
const toggleMethod = () => {
setMethod(method === 'login' ? 'signup' : 'login')
}
return (
<>
<AuthForm method={method} />
<a href='javascript:;' onClick={toggleMethod}>
{method === 'login'
? 'I don\'t have an account yet (go to sign up).'
: 'I already have an account (go to log in).'}
</a>
</>
)
}
const AuthForm = (props) => {
const history = useHistory()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (event) => {
event.preventDefault()
try {
if (props.method === 'signup') {
await signUp({ email, password })
}
await login(email, password)
history.push('/')
} catch (err) {
window.alert('Error:' + err.message)
}
}
return (
<form onSubmit={handleSubmit}>
<h2>Email</h2>
<input
type='text'
value={email}
onChange={e => setEmail(e.target.value)}
/>
<h2>Password</h2>
<input
type='password'
value={password}
onChange={e => setPassword(e.target.value)}
/>
<div>
<input type='submit' value={props.method === 'signup' ? 'Sign up' : 'Log in'} />
</div>
</form>
)
}

View File

@ -14,7 +14,7 @@ const MainPage = () => {
const { data: user } = useAuth()
if (!user) {
return <span> Please <Link to='/auth'>log in</Link>. </span>
return <span> Please <Link to='/login'>log in</Link>. </span>
}
return (

View File

@ -12,11 +12,6 @@ page Main {
component: import Main from "@ext/MainPage.js"
}
route "/auth" -> page Auth
page Auth {
component: import Auth from "@ext/AuthPage.js"
}
entity User {=psl
id Int @id @default(autoincrement())
email String @unique
@ -54,4 +49,4 @@ action updateTask {
dependencies {=json
"react-clock": "3.0.0"
json=}
json=}

View File

@ -19,4 +19,3 @@ const login = async (email, password) => {
}
export default login

View File

@ -0,0 +1,56 @@
import React, { useState } from 'react'
import { useHistory, Link } from 'react-router-dom'
import login from '../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>
<br/>
<span>
I don't have an account yet (<Link to="/signup">go to signup</Link>).
</span>
</>
)
}
return <LoginForm/>
}
export default Login

View File

@ -0,0 +1,67 @@
import React, { useState } from 'react'
import { useHistory, Link } from 'react-router-dom'
import signup from '../signup.js'
import login from '../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>
<br/>
<span>
I already have an account (<Link to="/login">go to login</Link>).
</span>
</>
)
}
return (
<>
<SignUpForm/>
</>
)
}
export default Signup

View File

@ -0,0 +1,13 @@
import config from '../config.js'
import api, { handleApiError } from '../api.js'
const signup = async (userFields) => {
try {
await api.post(config.apiUrl + '/auth/signup', userFields)
} catch (error) {
handleApiError(error)
}
}
export default signup

View File

@ -1,13 +1,11 @@
{{={= =}=}}
import Prisma from '@prisma/client'
import prisma from '../dbClient.js'
{=& jsFnImportStatement =}
{=! TODO: This template is exactly the same at the moment as one for queries,
consider in the future if it is worth removing this duplication. =}
const prisma = new Prisma.PrismaClient()
export default async (args, context) => {
context = { ...context, entities: {
{=# entities =}

View File

@ -2,12 +2,10 @@
import jwt from 'jsonwebtoken'
import SecurePassword from 'secure-password'
import util from 'util'
import Prisma from '@prisma/client'
import prisma from '../dbClient.js'
import { handleRejection } from '../utils.js'
const prisma = new Prisma.PrismaClient()
const jwtSign = util.promisify(jwt.sign)
const jwtVerify = util.promisify(jwt.verify)
@ -46,15 +44,11 @@ const auth = handleRejection(async (req, res, next) => {
next()
})
// TODO(matija): since this function is not doing much anymore, we can remove it.
// Make sure to replace its invocations with direct calls to prisma client's create().
// Github issue: https://github.com/wasp-lang/wasp/issues/150
export const createNewUser = async (userFields) => {
const hashedPassword = await hashPassword(userFields.password)
const newUser = await prisma.{= userEntityLower =}.create({
data: {
...userFields,
password: hashedPassword
},
})
const newUser = await prisma.{= userEntityLower =}.create({ data: userFields })
return newUser
}

View File

@ -0,0 +1,44 @@
{{={= =}=}}
import Prisma from '@prisma/client'
import { hashPassword } from './core/auth.js'
{=# isAuthEnabled =}
const PASSWORD_FIELD = 'password'
{=/ isAuthEnabled =}
const createDbClient = () => {
const prismaClient = new Prisma.PrismaClient()
{=# isAuthEnabled =}
prismaClient.$use(async (params, next) => {
// Make sure password is always hashed before storing to the database.
if (params.model === '{= userEntityUpper =}') {
if (['create', 'update', 'updateMany'].includes(params.action)) {
if (params.args.data.hasOwnProperty(PASSWORD_FIELD)) {
params.args.data[PASSWORD_FIELD] = await hashPassword(params.args.data[PASSWORD_FIELD])
}
} else if (params.action === 'upsert') {
if (params.args.create.data.hasOwnProperty(PASSWORD_FIELD)) {
params.args.create.data[PASSWORD_FIELD] =
await hashPassword(params.args.create.data[PASSWORD_FIELD])
}
if (params.args.update.data.hasOwnProperty(PASSWORD_FIELD)) {
params.args.update.data[PASSWORD_FIELD] =
await hashPassword(params.args.update.data[PASSWORD_FIELD])
}
}
}
const result = next(params)
return result
})
{=/ isAuthEnabled =}
return prismaClient
}
const dbClient = createDbClient()
export default dbClient

View File

@ -1,13 +1,11 @@
{{={= =}=}}
import Prisma from '@prisma/client'
import prisma from '../dbClient.js'
{=& jsFnImportStatement =}
{=! TODO: This template is exactly the same at the moment as one for queries,
consider in the future if it is worth removing this duplication. =}
const prisma = new Prisma.PrismaClient()
export default async (args, context) => {
context = { ...context, entities: {
{=# entities =}

View File

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

View File

@ -38,4 +38,3 @@ export default handleRejection(async (req, res) => {
return res.json({ token })
})

View File

@ -0,0 +1,10 @@
import { createNewUser } from '../../core/auth.js'
import { handleRejection } from '../../utils.js'
export default handleRejection(async (req, res) => {
const userFields = req.body || {}
await createNewUser(userFields)
res.send()
})

View File

@ -1,9 +1,4 @@
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) {

View File

@ -1,50 +0,0 @@
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,60 +0,0 @@
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

@ -19,8 +19,15 @@ export const getTask = async ({ id }, context) => {
}
const Task = context.entities.Task
const task = await Task.findOne(
{ where: { id, user: { id: context.user.id } } }
)
// NOTE(matija): we can't call findOne() with the specific user, so we have to fetch user first
// and then manually check.
const task = await Task.findOne({ where: { id }, include: { user: true } })
if (!task) {
throw new HttpError(404)
}
if (task.user.id !== context.user.id) {
throw new HttpError(403)
}
return task
}

View File

@ -22,16 +22,6 @@ entity Task {=psl
userId 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"
@ -68,11 +58,6 @@ query getTask {
// --------- 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

@ -2,7 +2,7 @@
# Cabal file when you run `stack build`. See the hpack website for help with
# this file: <https://github.com/sol/hpack>.
name: waspc
version: 0.1.7 # %WASP_VERSION% - annotation for new-release script.
version: 0.1.8 # %WASP_VERSION% - annotation for new-release script.
github: "Martinsos/waspc"
license: MIT
author: "wasp-lang"

View File

@ -4,9 +4,10 @@ module Generator.ServerGenerator
) where
import Data.Aeson (object, (.=))
import Data.Maybe (isJust)
import Data.Maybe (isJust, fromJust)
import Data.List (intercalate)
import qualified Path as P
import StrongPath ((</>))
import CompileOptions (CompileOptions)
import Generator.Common (nodeVersionAsText)
@ -24,6 +25,7 @@ import Generator.ServerGenerator.AuthG (genAuth)
import qualified NpmDependency as ND
import Wasp (Wasp, getAuth)
import qualified Wasp
import qualified Wasp.Auth
import qualified Wasp.NpmDependencies as WND
@ -94,12 +96,30 @@ genSrcDir wasp = concat
, [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|]]
, [genDbClient wasp]
, genRoutesDir wasp
, genOperationsRoutes wasp
, genOperations wasp
, genAuth wasp
]
genDbClient :: Wasp -> FileDraft
genDbClient wasp = C.makeTemplateFD tmplFile dstFile (Just tmplData)
where
maybeAuth = getAuth wasp
dbClientRelToSrcP = [P.relfile|dbClient.js|]
tmplFile = C.asTmplFile $ [P.reldir|src|] P.</> dbClientRelToSrcP
dstFile = C.serverSrcDirInServerRootDir </> (C.asServerSrcFile dbClientRelToSrcP)
tmplData =
if (isJust maybeAuth)
then object
[ "isAuthEnabled" .= True
, "userEntityUpper" .= (Wasp.Auth._userEntity $ fromJust maybeAuth)
]
else object []
genRoutesDir :: Wasp -> [FileDraft]
genRoutesDir wasp =
-- TODO(martin): We will probably want to extract "routes" path here same as we did with "src", to avoid hardcoding,

View File

@ -18,6 +18,7 @@ genAuth wasp = case maybeAuth of
-- Auth routes
, genAuthRoutesIndex
, genLoginRoute auth
, genSignupRoute
, genMeRoute auth
]
Nothing -> []
@ -52,6 +53,9 @@ genLoginRoute auth = C.makeTemplateFD tmplFile dstFile (Just tmplData)
, "userEntityLower" .= Util.toLowerFirst userEntity
]
genSignupRoute :: FileDraft
genSignupRoute = C.copySrcTmplAsIs (C.asTmplSrcFile [P.relfile|routes/auth/signup.js|])
genMeRoute :: Wasp.Auth.Auth -> FileDraft
genMeRoute auth = C.makeTemplateFD tmplFile dstFile (Just tmplData)
where

View File

@ -10,14 +10,19 @@ import Generator.WebAppGenerator.Common as C
genAuth :: Wasp -> [FileDraft]
genAuth wasp = case maybeAuth of
Just _ -> [ genLogin
Just _ -> [ genSignup
, genLogin
, genLogout
, genUseAuth
]
] ++ genAuthPages
Nothing -> []
where
maybeAuth = getAuth wasp
-- | Generates file with signup function to be used by Wasp developer.
genSignup :: FileDraft
genSignup = C.copyTmplAsIs (C.asTmplFile [P.relfile|src/auth/signup.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|])
@ -32,21 +37,14 @@ genLogout = C.copyTmplAsIs (C.asTmplFile [P.relfile|src/auth/logout.js|])
genUseAuth :: FileDraft
genUseAuth = C.copyTmplAsIs (C.asTmplFile [P.relfile|src/auth/useAuth.js|])
genAuthPages :: [FileDraft]
genAuthPages =
[ genSignupPage
, genLoginPage
]
{-
-- | 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)
]
genLoginPage :: FileDraft
genLoginPage = C.copyTmplAsIs (C.asTmplFile [P.relfile|src/auth/pages/Login.js|])
getUseUserDstFileName :: Wasp.Auth.Auth -> Maybe (P.Path P.Rel P.File)
getUseUserDstFileName a = P.parseRelFile ("use" ++ (Wasp.Auth._userEntity a) ++ ".js")
-}
genSignupPage :: FileDraft
genSignupPage = C.copyTmplAsIs (C.asTmplFile [P.relfile|src/auth/pages/Signup.js|])

View File

@ -2,6 +2,7 @@ module Generator.WebAppGenerator.RouterGenerator
( generateRouter
) where
import Data.Maybe (isJust)
import Data.Aeson (ToJSON (..), object, (.=))
import qualified Path as P
@ -51,13 +52,58 @@ generateRouter wasp = C.makeTemplateFD
createRouterTemplateData :: Wasp -> RouterTemplateData
createRouterTemplateData wasp = RouterTemplateData
{ _routes = Wasp.getRoutes wasp
, _pagesToImport = map createPageTemplateData $ Wasp.getPages wasp
{ _routes = routes
, _pagesToImport = pages
}
where
maybeAuth = Wasp.getAuth wasp
routes = (Wasp.getRoutes wasp) ++ (if isJust maybeAuth then authRoutes else [])
-- TODO(matija): It would be nicer if we were changing AST "higher" in the program, e.g.
-- adding built-in pages rather than doing it here in the generator -> that way we'd keep
-- generator code simpler and push the logic higher.
pages = (map createPageTemplateData $ Wasp.getPages wasp) ++
(if isJust maybeAuth then authPages else [])
authRoutes :: [Wasp.Route.Route]
authRoutes =
[ Wasp.Route.Route -- Signup route
{ Wasp.Route._urlPath = signupPageRoute
, Wasp.Route._targetPage = signupPageName
}
, Wasp.Route.Route -- Login route
{ Wasp.Route._urlPath = loginPageRoute
, Wasp.Route._targetPage = loginPageName
}
]
authPages :: [PageTemplateData]
authPages =
[ PageTemplateData -- Signup page
{ _importWhat = signupPageName
, _importFrom =
"./" ++ (SP.fromRelFileP $
SP.fromPathRelFileP [P.relfile|auth/pages/Signup.js|])
}
, PageTemplateData -- Login page
{ _importWhat = loginPageName
, _importFrom =
"./" ++ (SP.fromRelFileP $
SP.fromPathRelFileP [P.relfile|auth/pages/Login.js|])
}
]
signupPageName = "Signup"
signupPageRoute = "/signup"
loginPageName = "Login"
loginPageRoute = "/login"
createPageTemplateData :: Wasp.Page.Page -> PageTemplateData
createPageTemplateData page = PageTemplateData
{ _importFrom = relPathToExtSrcDir ++ SP.toFilePath (SP.relFileToPosix' $ Wasp.JsImport._from pageComponent)
{ _importFrom = relPathToExtSrcDir ++
SP.toFilePath (SP.relFileToPosix' $ Wasp.JsImport._from pageComponent)
, _importWhat = case Wasp.JsImport._namedImports pageComponent of
-- If no named imports, we go with the default import.
[] -> pageName