Changes on the frontend side

This commit is contained in:
theUniC
2021-11-05 19:56:29 +01:00
parent b36929dc66
commit 720fe36e9d
49 changed files with 4522 additions and 4338 deletions

View File

@@ -11,3 +11,6 @@ indent_size = 2
[.github/workflows/*.yaml]
indent_size = 2
[*.{ts,tsx}]
indent_size = 2

6
.env
View File

@@ -44,3 +44,9 @@ MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
# passwords that contain special characters (@, %, :, +) must be urlencoded
REDIS_URL=redis://localhost
###< snc/redis-bundle ###
###> lexik/jwt-authentication-bundle ###
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=925560e7309bd007e6827b45c2ff3166
###< lexik/jwt-authentication-bundle ###

4
.gitignore vendored
View File

@@ -27,3 +27,7 @@ yarn-error.log
###> php-cs-fixer ###
.php-cs-fixer.cache
###< php-cs-fixer ###
###> lexik/jwt-authentication-bundle ###
/config/jwt/*.pem
###< lexik/jwt-authentication-bundle ###

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
# Ignore artifacts:
build
coverage

1
.prettierrc.json Normal file
View File

@@ -0,0 +1 @@
{}

42
assets/api.ts Normal file
View File

@@ -0,0 +1,42 @@
import axios, { AxiosResponse } from "axios";
const ApiRoutes: { [key: string]: string } = {
token: "/api/login/check",
refreshToken: "/api/token/refresh",
postUser: "/api/users",
};
type RouteName = "token" | "refreshToken" | "postUser";
const apiUri = (routeName: RouteName): string => {
if (!ApiRoutes.hasOwnProperty(routeName)) {
throw new Error(`No API route "${routeName}" exists.`);
}
return ApiRoutes[routeName];
};
const get = async <T>(uri: string): Promise<AxiosResponse<T>> => axios.get(uri);
const post = async <T>(
uri: string,
data?: FormData
): Promise<AxiosResponse<T>> => axios.post(uri, data);
export interface SignupData {
email: string;
userName: string;
name: string;
location: string;
website: string;
password: string;
biography: string;
}
interface SignupResponse {}
export const signupUser = async (data: SignupData): Promise<SignupResponse> => {
const d = new FormData();
Object.entries(data).forEach(([value, key]) => d.append(key, value));
return post<SignupResponse>(apiUri("postUser"), d);
};

12
assets/app.js Normal file
View File

@@ -0,0 +1,12 @@
/*
* Welcome to your app's main JavaScript file!
*
* We recommend including the built version of this JavaScript file
* (and its CSS file) in your base layout (base.html.twig).
*/
// any CSS you import will output into a single css file (app.css in this case)
import "./styles/app.css";
// start the Stimulus application
import "./bootstrap";

13
assets/bootstrap.js vendored Normal file
View File

@@ -0,0 +1,13 @@
import { startStimulusApp } from "@symfony/stimulus-bridge";
// Registers Stimulus controllers from controllers.json and in the controllers/ directory
export const app = startStimulusApp(
require.context(
"@symfony/stimulus-bridge/lazy-controller-loader!./controllers",
true,
/\.(j|t)sx?$/
)
);
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);

21
assets/components/App.tsx Normal file
View File

@@ -0,0 +1,21 @@
import * as React from "react";
import { Route, HashRouter as Router, Switch } from "react-router-dom";
import Login from "../pages/Login";
import Signup from "../pages/Signup";
const App: React.FC = () => {
return (
<Router>
<Switch>
<Route path="/login">
<Login />
</Route>
<Route path="/signup">
<Signup />
</Route>
</Switch>
</Router>
);
};
export default App;

View File

@@ -0,0 +1,72 @@
import React from "react";
import logo from "../images/cheeper.svg";
interface Props {
height:
| "0"
| "px"
| "0.5"
| "1"
| "1.5"
| "2"
| "2.5"
| "3"
| "3.5"
| "4"
| "5"
| "6"
| "7"
| "8"
| "9"
| "10"
| "11"
| "12"
| "14"
| "16"
| "20"
| "24"
| "28"
| "32"
| "36"
| "40"
| "44"
| "48"
| "52"
| "56"
| "60"
| "64"
| "72"
| "80"
| "96"
| "auto"
| "1/2"
| "1/3"
| "2/3"
| "1/4"
| "2/4"
| "3/4"
| "1/5"
| "2/5"
| "3/5"
| "4/5"
| "1/6"
| "2/6"
| "3/6"
| "4/6"
| "5/6"
| "h-full"
| "h-screen";
additionalClasses?: string[];
}
const CheeperLogo: React.FC<Props> = ({ height, additionalClasses }) => (
<img
className={`mx-auto h-${height} w-auto ${
additionalClasses ? additionalClasses.join(" ") : ""
}`}
src={logo}
alt="Cheeper logo"
/>
);
export default CheeperLogo;

7
assets/context.ts Normal file
View File

@@ -0,0 +1,7 @@
import * as React from "react";
export interface Context {
apiDocUri?: string;
}
export const AppContext = React.createContext<Context>({});

4
assets/controllers.json Normal file
View File

@@ -0,0 +1,4 @@
{
"controllers": [],
"entrypoints": []
}

View File

@@ -0,0 +1,16 @@
import * as React from "react";
import { render } from "react-dom";
import { Controller } from "stimulus";
import App from "../components/App";
/* stimulusFetch: 'lazy' */
export default class extends Controller {
connect() {
render(
<React.StrictMode>
<App />
</React.StrictMode>,
this.element
);
}
}

View File

@@ -0,0 +1,18 @@
import { Controller } from "stimulus";
/*
* This is an example Stimulus controller!
*
* Any element with a data-controller="hello" attribute will cause
* this controller to be executed. The name "hello" comes from the filename:
* hello_controller.js -> "hello"
*
* Delete this file or adapt it for your use!
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
connect() {
this.element.textContent =
"Hello Stimulus! Edit me in assets/controllers/hello_controller.js";
}
}

View File

@@ -1,3 +1,5 @@
@import url('https://fonts.googleapis.com/css2?family=Jost&display=swap');
@import url("https://fonts.googleapis.com/css2?family=Jost&display=swap");
@import "~bulma/bulma";
.logo-text{font-family: 'Jost', sans-serif;}
.logo-text {
font-family: "Jost", sans-serif;
}

4
assets/custom.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "*.svg" {
const content: string;
export default content;
}

View File

@@ -1,14 +0,0 @@
import Vue from "vue";
import Buefy from "buefy";
import "@/../css/app.scss";
// import "buefy/dist/buefy.css";
import App from "@/components/App.vue";
import router from "@/router";
Vue.config.productionTip = false;
Vue.use(Buefy);
new Vue({
router,
render: (h) => h(App),
}).$mount("#app-root");

View File

@@ -1,13 +0,0 @@
<template>
<router-view></router-view>
</template>
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
@Component
export default class App extends Vue {
}
</script>

View File

@@ -1,4 +0,0 @@
declare module "*.svg" {
const content: string;
export default content;
}

View File

@@ -1,32 +0,0 @@
<template>
<section class="hero is-medium">
<div class="hero-body">
<div class="container has-text-centered">
<img width="64" :src="logo" alt="cheeper" />
<h1 class="is-size-2 has-text-info-dark logo-text">
Cheeper
</h1>
<h2>A twitter's clone</h2>
<p class="buttons is-centered mt-4">
<a href="#" class="button is-link">
<span>Sign Up</span>
</a>
<a href="#" class="button is-link">
<span>Log in</span>
</a>
</p>
</div>
</div>
</section>
</template>
<script lang="ts">
import Vue from "vue";
import Component from "vue-class-component";
import logo from "@/../images/cheeper.svg";
@Component
export default class Home extends Vue {
logo = logo;
}
</script>

View File

@@ -1,19 +0,0 @@
import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router";
import Home from "@/pages/Home.vue";
Vue.use(VueRouter);
const routes: Array<RouteConfig> = [
{
path: "/",
name: "Home",
component: Home,
},
];
const router = new VueRouter({
routes,
});
export default router;

View File

@@ -1,4 +0,0 @@
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}

101
assets/pages/Login.tsx Normal file
View File

@@ -0,0 +1,101 @@
import * as React from "react";
import { LockClosedIcon } from "@heroicons/react/solid";
import CheeperLogo from "../components/CheeperLogo";
const Login: React.FC = () => (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<CheeperLogo height="48" />
<h2 className="mt-6 text-center jost text-3xl font-extrabold text-gray-900">
Sign in to your account
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Or{" "}
<a
href="#"
className="font-medium text-cheeper-dark-blue hover:text-cheeper-blue"
>
Create a new one
</a>
</p>
</div>
<form className="mt-8 space-y-6" action="#" method="POST">
<input type="hidden" name="remember" defaultValue="true" />
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email-address" className="sr-only">
Email address
</label>
<input
id="email-address"
name="email"
type="email"
autoComplete="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-cheeper-dark-blue focus:border-cheeper-dark-blue focus:z-10 sm:text-sm"
placeholder="Email address"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-cheeper-dark-blue focus:border-cheeper-dark-blue focus:z-10 sm:text-sm"
placeholder="Password"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="h-4 w-4 text-cheeper-dark-blue focus:ring-cheeper-blue border-gray-300 rounded"
/>
<label
htmlFor="remember-me"
className="ml-2 block text-sm text-gray-900"
>
Remember me
</label>
</div>
<div className="text-sm">
<a
href="#"
className="font-medium text-cheeper-dark-blue hover:text-cheeper-blue"
>
Forgot your password?
</a>
</div>
</div>
<div>
<button
type="submit"
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-cheeper-dark-blue hover:bg-cheeper-blue focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
<LockClosedIcon
className="h-5 w-5 text-cheeper-blue group-hover:text-cheeper-dark-blue"
aria-hidden="true"
/>
</span>
Sign in
</button>
</div>
</form>
</div>
</div>
);
export default Login;

228
assets/pages/Signup.tsx Normal file
View File

@@ -0,0 +1,228 @@
import React, { useState } from "react";
import * as Api from "../api";
import CheeperLogo from "../components/CheeperLogo";
import * as s from "superstruct";
const Signup: React.FC = () => {
const [email, setEmail] = useState<string>("");
const [userName, setUsername] = useState<string>("");
const [name, setName] = useState<string>("");
const [location, setLocation] = useState<string>("");
const [website, setWebsite] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [biography, setBiography] = useState<string>("");
const handleSubmit = async () => {
const data: Api.SignupData = {
email,
userName,
name,
location,
website,
password,
biography,
};
const signupSchema = s.object({
email: s.string(),
userName: s.string(),
name: s.string(),
location: s.optional(s.string()),
website: s.optional(s.string()),
password: s.string(),
biography: s.optional(s.string()),
});
try {
s.assert(data, signupSchema);
await Api.signupUser(data);
} catch (error) {}
};
return (
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl w-full space-y-8">
<div>
<div className="md:grid md:grid-cols-3 md:gap-6">
<div className="md:col-span-1">
<div className="px-4 sm:px-0">
<CheeperLogo height="28" additionalClasses={["mb-5"]} />
<h3 className="text-lg font-medium leading-6 text-gray-900">
Sign up
</h3>
<p className="mt-1 text-sm text-gray-600">
Fill the form to create a new account.
</p>
</div>
</div>
<div className="mt-5 md:mt-0 md:col-span-2">
<div className="shadow sm:rounded-md sm:overflow-hidden">
<form onSubmit={handleSubmit}>
<div className="px-4 py-5 bg-white space-y-6 sm:p-6">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start">
<label
htmlFor="username"
className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
Username
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm">
cheeper.com/
</span>
<input
type="text"
name="username"
id="username"
value={userName}
className="flex-1 block w-full focus:ring-cheeper-dark-blue focus:border-cheeper-blue min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300"
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<span className="text-red-400">Holaaa!!!</span>
</div>
</div>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
Password
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<input
type="password"
name="password"
id="password"
value={password}
className="block max-w-lg w-full shadow-sm focus:ring-cheeper-blue focus:border-cheeper-dark-blue sm:text-sm border-gray-300 rounded-md"
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
</div>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
Email address
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<input
id="email"
name="email"
type="email"
className="block max-w-lg w-full shadow-sm focus:ring-cheeper-blue focus:border-cheeper-dark-blue sm:text-sm border-gray-300 rounded-md"
onChange={(e) => setEmail(e.target.value)}
value={email}
/>
</div>
</div>
</div>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label
htmlFor="name"
className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
Full name
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<input
type="text"
name="name"
id="name"
value={name}
className="block max-w-lg w-full shadow-sm focus:ring-cheeper-blue focus:border-cheeper-dark-blue sm:text-sm border-gray-300 rounded-md"
onChange={(e) => setName(e.target.value)}
/>
</div>
</div>
</div>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label
htmlFor="biography"
className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
Biography
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<textarea
id="biography"
name="biography"
rows={3}
className="max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md"
value={biography}
onChange={(e) => setBiography(e.target.value)}
/>
<p className="mt-2 text-sm text-gray-500">
Write a few sentences about yourself.
</p>
</div>
</div>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label
htmlFor="location"
className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
Location
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<input
type="text"
name="location"
id="location"
value={location}
className="block max-w-lg w-full shadow-sm focus:ring-cheeper-blue focus:border-cheeper-dark-blue sm:text-sm border-gray-300 rounded-md"
onChange={(e) => setLocation(e.target.value)}
/>
</div>
</div>
</div>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label
htmlFor="website"
className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
Website
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<input
type="url"
name="website"
id="website"
value={website}
className="block max-w-lg w-full shadow-sm focus:ring-cheeper-blue focus:border-cheeper-dark-blue sm:text-sm border-gray-300 rounded-md"
onChange={(e) => setWebsite(e.target.value)}
/>
</div>
</div>
</div>
</div>
<div className="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Save
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Signup;

7
assets/styles/app.css Normal file
View File

@@ -0,0 +1,7 @@
@import url("https://fonts.googleapis.com/css2?family=Jost&display=swap");
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
.jost {
font-family: "Jost", sans-serif;
}

View File

@@ -13,6 +13,8 @@
"doctrine/doctrine-bundle": "^2.4",
"doctrine/doctrine-migrations-bundle": "^3.1",
"doctrine/orm": "^2.9",
"gesdinet/jwt-refresh-token-bundle": "dev-master",
"lexik/jwt-authentication-bundle": "^2.13",
"lstrojny/functional-php": "^1.11",
"nelmio/cors-bundle": "^2.0",
"ramsey/uuid": "^4.0",

400
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "54b1cea1472b4b3f694611131c5eb968",
"content-hash": "17dc82270cb045d352296de4474042aa",
"packages": [
{
"name": "api-platform/core",
@@ -1795,6 +1795,87 @@
],
"time": "2021-05-22T16:11:15+00:00"
},
{
"name": "gesdinet/jwt-refresh-token-bundle",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/markitosgv/JWTRefreshTokenBundle.git",
"reference": "458518cd6bbf2ecfa1729fded538a78a89a16cfa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/markitosgv/JWTRefreshTokenBundle/zipball/458518cd6bbf2ecfa1729fded538a78a89a16cfa",
"reference": "458518cd6bbf2ecfa1729fded538a78a89a16cfa",
"shasum": ""
},
"require": {
"lexik/jwt-authentication-bundle": "^2.0",
"php": ">=7.4",
"symfony/config": "^3.4|^4.4|^5.2",
"symfony/console": "^3.4|^4.4|^5.2",
"symfony/dependency-injection": "^3.4|^4.4|^5.2",
"symfony/deprecation-contracts": "^2.1",
"symfony/event-dispatcher": "^3.4|^4.4|^5.2",
"symfony/http-foundation": "^3.4|^4.4|^5.2",
"symfony/http-kernel": "^3.4|^4.4|^5.2",
"symfony/polyfill-php80": "^1.15",
"symfony/property-access": "^3.4|^4.4|^5.2",
"symfony/security-bundle": "^3.4|^4.0|^5.2",
"symfony/security-core": "^3.4|^4.4|^5.2",
"symfony/security-guard": "^3.4|^4.4|^5.2",
"symfony/security-http": "^3.4|^4.4|^5.2"
},
"conflict": {
"doctrine/persistence": "<1.3.3"
},
"require-dev": {
"akeneo/phpspec-skip-example-extension": "^4.0|^5.0",
"doctrine/cache": "^1.11|^2.0",
"doctrine/doctrine-bundle": "^1.12|^2.0",
"doctrine/mongodb-odm-bundle": "^3.4|^4.0",
"doctrine/orm": "^2.7",
"doctrine/persistence": "^1.3.3|^2.0",
"matthiasnoback/symfony-dependency-injection-test": "^4.0",
"phpspec/phpspec": "^7.0",
"phpunit/phpunit": "^8.5|^9.5",
"symfony/cache": "^3.4|^4.4|^5.2"
},
"default-branch": true,
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"psr-4": {
"Gesdinet\\JWTRefreshTokenBundle\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marcos Gómez Vilches",
"email": "marcos@gesdinet.com"
}
],
"description": "Implements a refresh token system over Json Web Tokens in Symfony",
"keywords": [
"jwt refresh token bundle symfony json web"
],
"support": {
"issues": "https://github.com/markitosgv/JWTRefreshTokenBundle/issues",
"source": "https://github.com/markitosgv/JWTRefreshTokenBundle/tree/master"
},
"time": "2021-08-12T11:55:47+00:00"
},
{
"name": "laminas/laminas-code",
"version": "4.4.2",
@@ -1862,6 +1943,252 @@
],
"time": "2021-07-09T11:58:40+00:00"
},
{
"name": "lcobucci/clock",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/lcobucci/clock.git",
"reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lcobucci/clock/zipball/353d83fe2e6ae95745b16b3d911813df6a05bfb3",
"reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"infection/infection": "^0.17",
"lcobucci/coding-standard": "^6.0",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-deprecation-rules": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/php-code-coverage": "9.1.4",
"phpunit/phpunit": "9.3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Lcobucci\\Clock\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Luís Cobucci",
"email": "lcobucci@gmail.com"
}
],
"description": "Yet another clock abstraction",
"support": {
"issues": "https://github.com/lcobucci/clock/issues",
"source": "https://github.com/lcobucci/clock/tree/2.0.x"
},
"funding": [
{
"url": "https://github.com/lcobucci",
"type": "github"
},
{
"url": "https://www.patreon.com/lcobucci",
"type": "patreon"
}
],
"time": "2020-08-27T18:56:02+00:00"
},
{
"name": "lcobucci/jwt",
"version": "4.1.4",
"source": {
"type": "git",
"url": "https://github.com/lcobucci/jwt.git",
"reference": "71cf170102c8371ccd933fa4df6252086d144de6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lcobucci/jwt/zipball/71cf170102c8371ccd933fa4df6252086d144de6",
"reference": "71cf170102c8371ccd933fa4df6252086d144de6",
"shasum": ""
},
"require": {
"ext-hash": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"ext-sodium": "*",
"lcobucci/clock": "^2.0",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"infection/infection": "^0.21",
"lcobucci/coding-standard": "^6.0",
"mikey179/vfsstream": "^1.6.7",
"phpbench/phpbench": "^1.0@alpha",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-deprecation-rules": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/php-invoker": "^3.1",
"phpunit/phpunit": "^9.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Lcobucci\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Luís Cobucci",
"email": "lcobucci@gmail.com",
"role": "Developer"
}
],
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
"keywords": [
"JWS",
"jwt"
],
"support": {
"issues": "https://github.com/lcobucci/jwt/issues",
"source": "https://github.com/lcobucci/jwt/tree/4.1.4"
},
"funding": [
{
"url": "https://github.com/lcobucci",
"type": "github"
},
{
"url": "https://www.patreon.com/lcobucci",
"type": "patreon"
}
],
"time": "2021-03-23T23:53:08+00:00"
},
{
"name": "lexik/jwt-authentication-bundle",
"version": "v2.13.0",
"source": {
"type": "git",
"url": "https://github.com/lexik/LexikJWTAuthenticationBundle.git",
"reference": "d3dbf97f9a2f7cead53767111234bb16b8726681"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lexik/LexikJWTAuthenticationBundle/zipball/d3dbf97f9a2f7cead53767111234bb16b8726681",
"reference": "d3dbf97f9a2f7cead53767111234bb16b8726681",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"lcobucci/jwt": "^3.4|^4.0",
"namshi/jose": "^7.2",
"php": ">=7.1",
"symfony/deprecation-contracts": "^2.4",
"symfony/framework-bundle": "^4.4|^5.1",
"symfony/security-bundle": "^4.4|^5.1"
},
"require-dev": {
"symfony/browser-kit": "^4.4|^5.1",
"symfony/console": "^4.4|^5.1",
"symfony/dom-crawler": "^4.4|^5.1",
"symfony/phpunit-bridge": "^4.4|^5.1",
"symfony/var-dumper": "^4.4|^5.1",
"symfony/yaml": "^4.4|^5.1"
},
"suggest": {
"gesdinet/jwt-refresh-token-bundle": "Implements a refresh token system over Json Web Tokens in Symfony",
"spomky-labs/lexik-jose-bridge": "Provides a JWT Token encoder with encryption support"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"Lexik\\Bundle\\JWTAuthenticationBundle\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jeremy Barthe",
"email": "j.barthe@lexik.fr",
"homepage": "https://github.com/jeremyb"
},
{
"name": "Nicolas Cabot",
"email": "n.cabot@lexik.fr",
"homepage": "https://github.com/slashfan"
},
{
"name": "Cedric Girard",
"email": "c.girard@lexik.fr",
"homepage": "https://github.com/cedric-g"
},
{
"name": "Dev Lexik",
"email": "dev@lexik.fr",
"homepage": "https://github.com/lexik"
},
{
"name": "Robin Chalas",
"email": "robin.chalas@gmail.com",
"homepage": "https://github.com/chalasr"
},
{
"name": "Lexik Community",
"homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle/graphs/contributors"
}
],
"description": "This bundle provides JWT authentication for your Symfony REST API",
"homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle",
"keywords": [
"Authentication",
"JWS",
"api",
"bundle",
"jwt",
"rest",
"symfony"
],
"support": {
"issues": "https://github.com/lexik/LexikJWTAuthenticationBundle/issues",
"source": "https://github.com/lexik/LexikJWTAuthenticationBundle/tree/v2.13.0"
},
"funding": [
{
"url": "https://github.com/chalasr",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/lexik/jwt-authentication-bundle",
"type": "tidelift"
}
],
"time": "2021-09-15T16:40:37+00:00"
},
{
"name": "lstrojny/functional-php",
"version": "1.17.0",
@@ -2016,6 +2343,73 @@
],
"time": "2021-03-07T00:25:34+00:00"
},
{
"name": "namshi/jose",
"version": "7.2.3",
"source": {
"type": "git",
"url": "https://github.com/namshi/jose.git",
"reference": "89a24d7eb3040e285dd5925fcad992378b82bcff"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/namshi/jose/zipball/89a24d7eb3040e285dd5925fcad992378b82bcff",
"reference": "89a24d7eb3040e285dd5925fcad992378b82bcff",
"shasum": ""
},
"require": {
"ext-date": "*",
"ext-hash": "*",
"ext-json": "*",
"ext-pcre": "*",
"ext-spl": "*",
"php": ">=5.5",
"symfony/polyfill-php56": "^1.0"
},
"require-dev": {
"phpseclib/phpseclib": "^2.0",
"phpunit/phpunit": "^4.5|^5.0",
"satooshi/php-coveralls": "^1.0"
},
"suggest": {
"ext-openssl": "Allows to use OpenSSL as crypto engine.",
"phpseclib/phpseclib": "Allows to use Phpseclib as crypto engine, use version ^2.0."
},
"type": "library",
"autoload": {
"psr-4": {
"Namshi\\JOSE\\": "src/Namshi/JOSE/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Alessandro Nadalin",
"email": "alessandro.nadalin@gmail.com"
},
{
"name": "Alessandro Cinelli (cirpo)",
"email": "alessandro.cinelli@gmail.com"
}
],
"description": "JSON Object Signing and Encryption library for PHP.",
"keywords": [
"JSON Web Signature",
"JSON Web Token",
"JWS",
"json",
"jwt",
"token"
],
"support": {
"issues": "https://github.com/namshi/jose/issues",
"source": "https://github.com/namshi/jose/tree/master"
},
"time": "2016-12-05T07:27:31+00:00"
},
{
"name": "nelmio/cors-bundle",
"version": "2.1.1",
@@ -11360,7 +11754,9 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {
"gesdinet/jwt-refresh-token-bundle": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {

View File

@@ -17,4 +17,6 @@ return [
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Snc\RedisBundle\SncRedisBundle::class => ['all' => true],
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
Gesdinet\JWTRefreshTokenBundle\GesdinetJWTRefreshTokenBundle::class => ['all' => true],
];

View File

@@ -0,0 +1,4 @@
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'

View File

@@ -1,23 +1,53 @@
security:
enable_authenticator_manager: true
password_hashers:
App\Entity\User:
algorithm: auto
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
users_in_memory: { memory: null }
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: lazy
provider: users_in_memory
lazy: true
provider: app_user_provider
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
api_token_refresh:
pattern: ^/api/token/refresh
stateless: true
refresh_jwt: ~
login:
pattern: ^/api/login
stateless: true
json_login:
check_path: /api/login/check
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api:
pattern: ^/api
stateless: true
jwt: ~
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
- { path: ^/api/login, roles: PUBLIC_ACCESS }
- { path: ^/api/token/refresh, roles: PUBLIC_ACCESS }
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
- { path: ^/api/users, roles: PUBLIC_ACCESS }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }

View File

@@ -4,16 +4,26 @@ webpack_encore:
# If multiple builds are defined (as shown below), you can disable the default build:
# output_path: false
# if using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
# Set attributes that will be rendered on all script and link tags
script_attributes:
defer: true
# Uncomment (also under link_attributes) if using Turbo Drive
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
# 'data-turbo-track': reload
# link_attributes:
# Uncomment if using Turbo Drive
# 'data-turbo-track': reload
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
# crossorigin: 'anonymous'
# preload all rendered script and link tags automatically via the http2 Link header
# Preload all rendered script and link tags automatically via the HTTP/2 Link header
# preload: true
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
# strict_mode: false
# if you have multiple builds:
# If you have multiple builds:
# builds:
# pass "frontend" as the 3rg arg to the Twig functions
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}

View File

@@ -0,0 +1,2 @@
gesdinet_jwt_refresh_token:
path: /api/token/refresh

View File

@@ -0,0 +1,2 @@
api_login_check:
path: /api/login/check

View File

@@ -89,3 +89,8 @@ services:
- { name: messenger.message_handler, bus: event.bus }
class: 'Cheeper\Chapter6\Infrastructure\Application\Projector\Author\SymfonyAuthorFollowedHandler'
App\EventListener\UserCreatedListener:
tags:
- name: "doctrine.orm.entity_listener"
event: "postPersist"
entity: 'App\Entity\User'

View File

@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20210918175017 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('CREATE TABLE architecture_followers (user_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', followers INT NOT NULL, PRIMARY KEY(user_id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB COMMENT = \'\' ');
$this->addSql('CREATE TABLE authors (author_id VARCHAR(36) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, user_name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, email VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, name VARCHAR(100) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, biography LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, location VARCHAR(100) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, website VARCHAR(100) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, birth_date DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\', PRIMARY KEY(author_id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB COMMENT = \'\' ');
$this->addSql('CREATE TABLE cheeps (cheep_id VARCHAR(36) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, author_id VARCHAR(36) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, cheep_message_message VARCHAR(260) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, cheep_date_date DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(cheep_id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB COMMENT = \'\' ');
$this->addSql('CREATE TABLE follows (follow_id VARCHAR(36) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, from_author_id VARCHAR(36) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, to_author_id VARCHAR(36) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, PRIMARY KEY(follow_id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB COMMENT = \'\' ');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('DROP TABLE architecture_followers');
$this->addSql('DROP TABLE authors');
$this->addSql('DROP TABLE cheeps');
$this->addSql('DROP TABLE follows');
}
}

View File

@@ -1,31 +1,32 @@
{
"devDependencies": {
"@api-platform/api-doc-parser": "^0.12.0",
"@babel/preset-react": "^7.0.0",
"@fortawesome/fontawesome-free": "^5.13.0",
"@symfony/webpack-encore": "^0.30.0",
"@typescript-eslint/eslint-plugin": "^3.3.0",
"@typescript-eslint/parser": "^3.3.0",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^5.0.2",
"buefy": "^0.8.20",
"bulma": "^0.9.0",
"@heroicons/react": "^1.0.4",
"@symfony/stimulus-bridge": "^2.0.0",
"@symfony/webpack-encore": "^1.0.0",
"@tailwindcss/forms": "^0.3.3",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.3.0",
"autoprefixer": "^10.3.4",
"axios": "^0.21.4",
"core-js": "^3.0.0",
"eslint": "^7.2.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-vue": "^6.2.2",
"fork-ts-checker-webpack-plugin": "^4.0.0",
"node-sass": "^4.14.1",
"prettier": "^2.0.5",
"postcss": "^8.3.6",
"postcss-loader": "^6.0.0",
"prettier": "2.4.1",
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.3.0",
"regenerator-runtime": "^0.13.2",
"sass-loader": "^8.0.0",
"sort-package-json": "^1.44.0",
"ts-loader": "^5.3.0",
"tsconfig-paths-webpack-plugin": "^3.2.0",
"typescript": "^3.9.5",
"vue": "^2.6.11",
"vue-class-component": "^7.2.3",
"vue-loader": "^15.9.2",
"vue-router": "^3.3.4",
"vue-template-compiler": "^2.6.11",
"stimulus": "^2.0.0",
"superstruct": "^0.15.2",
"tailwindcss": "^2.2.15",
"ts-loader": "^9.0.0",
"typescript": "^4.4.3",
"webpack-notifier": "^1.6.0"
},
"license": "UNLICENSED",
@@ -35,6 +36,5 @@
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress"
},
"dependencies": {}
}
}

10
postcss.config.js Normal file
View File

@@ -0,0 +1,10 @@
module.exports = {
plugins: {
// include whatever plugins you want
// but make sure you install these via yarn or npm!
// add browserslist config to package.json (see below)
tailwindcss: {},
autoprefixer: {}
}
}

View File

@@ -12,7 +12,7 @@ use Cheeper\DomainModel\Follow\Follow;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
class AuthorsFixtures extends Fixture
final class AuthorsFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
final class UsersFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
// TODO: Implement load() method.
}
}

212
src/App/Entity/User.php Normal file
View File

@@ -0,0 +1,212 @@
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\UserRepository;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Doctrine\UuidBinaryType;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[UniqueEntity("email")]
#[ApiResource(collectionOperations: ["post"], itemOperations: ["get"])]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id, ORM\GeneratedValue(strategy: "NONE"), ORM\Column(type: UuidBinaryType::NAME)]
private UuidInterface $id;
#[ORM\Column(type: Types::STRING, length: 180, unique: true)]
#[Assert\NotBlank, Assert\Email]
private ?string $email = null;
#[ORM\Column(type: Types::JSON)]
private array $roles = [];
#[ORM\Column(type: Types::STRING)]
#[Assert\NotBlank]
private ?string $userName = null;
#[ORM\Column(type: Types::STRING)]
#[Assert\NotBlank]
private ?string $name = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $biography = null;
#[ORM\Column(type: Types::STRING)]
private ?string $location = null;
#[ORM\Column(type: Types::STRING)]
#[Assert\Url]
private ?string $website = null;
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
#[Assert\Date]
private ?DateTimeImmutable $birthDate = null;
#[ORM\Column]
#[Assert\NotCompromisedPassword]
private ?string $password = null;
public function __construct()
{
$this->id = Uuid::uuid4();
}
public function getId(): UuidInterface
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->email;
}
/**
* @deprecated since Symfony 5.3, use getUserIdentifier instead
*/
public function getUsername(): string
{
return $this->userName;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* @see PasswordAuthenticatedUserInterface
*/
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(?string $name): self
{
$this->name = $name;
return $this;
}
public function getBiography(): ?string
{
return $this->biography;
}
public function setBiography(?string $biography): self
{
$this->biography = $biography;
return $this;
}
public function getLocation(): ?string
{
return $this->location;
}
public function setLocation(?string $location): self
{
$this->location = $location;
return $this;
}
public function getWebsite(): ?string
{
return $this->website;
}
public function setWebsite(?string $website): self
{
$this->website = $website;
return $this;
}
public function getBirthDate(): ?DateTimeImmutable
{
return $this->birthDate;
}
public function setBirthDate(?DateTimeImmutable $birthDate): self
{
$this->birthDate = $birthDate;
return $this;
}
/**
* Returning a salt is only needed, if you are not using a modern
* hashing algorithm (e.g. bcrypt or sodium) in your security.yaml.
*
* @see UserInterface
*/
public function getSalt(): ?string
{
return null;
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\EventListener;
use App\Entity\User;
use App\Messenger\CommandBus;
use Cheeper\Application\Command\Author\SignUpBuilder;
use Doctrine\ORM\Event\LifecycleEventArgs;
final class UserCreatedListener
{
public function __construct(
private CommandBus $commandBus
) {
}
public function postPersist(User $user, LifecycleEventArgs $event): void
{
$signUpBuilder = SignUpBuilder::create(
authorId: $user->getId()->toString(),
userName: $user->getUserIdentifier(),
email: $user->getEmail()
);
$this->commandBus->handle(
$signUpBuilder
->biography($user->getBiography())
->birthDate($user->getBirthDate()?->format('Y-m-d'))
->location($user->getLocation())
->name($user->getName())
->website($user->getWebsite())
->build()
);
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
/**
* @method User|null find($id, $lockMode = null, $lockVersion = null)
* @method User|null findOneBy(array $criteria, array $orderBy = null)
* @method User[] findAll()
* @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
/**
* Used to upgrade (rehash) the user's password automatically over time.
*/
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user)));
}
$user->setPassword($newHashedPassword);
$this->_em->persist($user);
$this->_em->flush();
}
// /**
// * @return User[] Returns an array of User objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('u')
->andWhere('u.exampleField = :val')
->setParameter('val', $value)
->orderBy('u.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?User
{
return $this->createQueryBuilder('u')
->andWhere('u.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Cheeper\Chapter6\Infrastructure\Application\Projector\Author\Saga;
namespace Cheeper\Chapter6\Infrastructure\Application\Projector\Author\SagaStyle;
use App\Messenger\CommandBus;
use Cheeper\Chapter6\Application\Projector\Author\CountFollowers;
@@ -32,4 +32,4 @@ final class SymfonyAuthorFollowedHandler implements MessageSubscriberInterface
);
}
}
//end-snippet
//end-snippet

View File

@@ -149,6 +149,9 @@
"fzaninotto/faker": {
"version": "v1.9.1"
},
"gesdinet/jwt-refresh-token-bundle": {
"version": "v0.12.0"
},
"hamcrest/hamcrest-php": {
"version": "v2.0.0"
},
@@ -164,6 +167,24 @@
"laminas/laminas-zendframework-bridge": {
"version": "1.0.1"
},
"lcobucci/clock": {
"version": "2.0.0"
},
"lcobucci/jwt": {
"version": "4.1.4"
},
"lexik/jwt-authentication-bundle": {
"version": "2.5",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "2.5",
"ref": "5b2157bcd5778166a5696e42f552ad36529a07a6"
},
"files": [
"config/packages/lexik_jwt_authentication.yaml"
]
},
"lstrojny/functional-php": {
"version": "1.11.0"
},
@@ -176,6 +197,9 @@
"myclabs/deep-copy": {
"version": "1.9.5"
},
"namshi/jose": {
"version": "7.2.3"
},
"nelmio/cors-bundle": {
"version": "1.5",
"recipe": {
@@ -671,16 +695,19 @@
]
},
"symfony/webpack-encore-bundle": {
"version": "1.6",
"version": "1.9",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "1.6",
"ref": "69e1d805ad95964088bd510c05995e87dc391564"
"version": "1.9",
"ref": "0f274572ea315eb3b5884518a50ca43f211b4534"
},
"files": [
"assets/css/app.css",
"assets/js/app.js",
"assets/app.js",
"assets/bootstrap.js",
"assets/controllers.json",
"assets/controllers/hello_controller.js",
"assets/styles/app.css",
"config/packages/assets.yaml",
"config/packages/prod/webpack_encore.yaml",
"config/packages/test/webpack_encore.yaml",

19
tailwind.config.js Normal file
View File

@@ -0,0 +1,19 @@
module.exports = {
purge: [],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
container: {
center: true,
},
colors: {
"cheeper-dark-blue": "#82CCEB",
"cheeper-blue": "#AFEAF9",
},
},
},
variants: {
extend: {},
},
plugins: [require("@tailwindcss/forms")],
};

View File

@@ -5,7 +5,7 @@
{% endblock %}
{% block content %}
<div id="app-root" data-api-doc-uri="{{ url('api_doc', { _format: "json" }) }}"{% for attribute, value in attributes|default([]) %} data-{{ attribute }}="{{ value }}"{% endfor %}></div>
<div data-controller="app" data-app-apidocuri-value="{{ url('api_doc', { _format: "json", spec_version: "3" }) }}"{% for attribute, value in attributes|default([]) %} data-app-{{ attribute }}-value="{{ value }}"{% endfor %}></div>
{% endblock %}
{% block javascripts %}

View File

@@ -13,27 +13,13 @@
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": [
"assets/js/*"
]
},
"jsx": "react",
"allowJs": true,
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"assets/js/**/*.ts",
"assets/js/**/*.tsx",
"assets/js/**/*.vue"
// "tests/**/*.ts",
// "tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}
}

View File

@@ -1,6 +1,4 @@
const Encore = require("@symfony/webpack-encore");
const path = require("path");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
@@ -19,15 +17,13 @@ Encore
/*
* ENTRY CONFIG
*
* Add 1 entry for each "page" of your app
* (including one that's included on every page - e.g. "app")
*
* Each entry will result in one JavaScript file (e.g. app.js)
* and one CSS file (e.g. app.css) if your JavaScript imports CSS.
*/
.addEntry("app", "./assets/js/app.ts")
//.addEntry('page1', './assets/js/page1.js')
//.addEntry('page2', './assets/js/page2.js')
.addEntry("app", "./assets/app.js")
// enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
.enableStimulusBridge("./assets/controllers.json")
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
.splitEntryChunks()
@@ -49,6 +45,10 @@ Encore
// enables hashed filenames (e.g. app.abc123.css)
.enableVersioning(Encore.isProduction())
.configureBabel((config) => {
config.plugins.push("@babel/plugin-proposal-class-properties");
})
// enables @babel/preset-env polyfills
.configureBabelPresetEnv((config) => {
config.useBuiltIns = "usage";
@@ -56,11 +56,13 @@ Encore
})
// enables Sass/SCSS support
.enableSassLoader()
// .enableSassLoader()
// uncomment if you use TypeScript
.enableTypeScriptLoader()
.enableForkedTypeScriptTypesChecking()
// uncomment if you use React
.enableReactPreset()
// uncomment to get integrity="..." attributes on your script & link tags
// requires WebpackEncoreBundle 1.4 or higher
@@ -73,9 +75,6 @@ Encore
//.enableReactPreset()
//.addEntry('admin', './assets/js/admin.js')
.enableVueLoader(() => {}, { runtimeCompilerBuild: false });
.enablePostCssLoader();
const webpackConfig = Encore.getWebpackConfig();
webpackConfig.resolve.plugins = [new TsconfigPathsPlugin()];
module.exports = webpackConfig;
module.exports = Encore.getWebpackConfig();

7195
yarn.lock

File diff suppressed because it is too large Load Diff