commit
321710a5d8
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
config.yml
|
config.yml
|
||||||
|
*.log
|
||||||
|
41
Makefile
41
Makefile
@ -1,4 +1,4 @@
|
|||||||
.PHONY: docker migration dev clean test build-prepare build-js build-css build-font build-go copy-templates copy-images watch-js watch-css watch-templates watch
|
.PHONY: docker migration dev clean test
|
||||||
|
|
||||||
DB_USER=test
|
DB_USER=test
|
||||||
DB_PASS=test
|
DB_PASS=test
|
||||||
@ -13,48 +13,13 @@ DOCKER_COMPOSE_FILE=./docker/docker-compose.yml
|
|||||||
DOCKER_COMPOSE=docker-compose -f $(DOCKER_COMPOSE_FILE)
|
DOCKER_COMPOSE=docker-compose -f $(DOCKER_COMPOSE_FILE)
|
||||||
|
|
||||||
build-prepare:
|
build-prepare:
|
||||||
mkdir -p ./build/public/js
|
|
||||||
mkdir -p ./build/public/css
|
|
||||||
mkdir -p ./build/templates
|
|
||||||
mkdir -p ./build/public/img/movies
|
mkdir -p ./build/public/img/movies
|
||||||
|
|
||||||
build-js: build-prepare
|
build: build-prepare
|
||||||
node node_modules/.bin/browserify src/public/js/app.js -o build/public/js/app.js
|
|
||||||
|
|
||||||
build-css: build-prepare
|
|
||||||
node ./node_modules/.bin/lessc --include-path=./node_modules src/public/less/app.less build/public/css/app.css
|
|
||||||
|
|
||||||
build-font: build-prepare
|
|
||||||
node ./node_modules/.bin/fontify
|
|
||||||
|
|
||||||
build-go: build-prepare
|
|
||||||
go build -o ./build/canape src/main.go
|
go build -o ./build/canape src/main.go
|
||||||
|
|
||||||
copy-templates: build-prepare
|
|
||||||
cp -r ./src/templates/* ./build/templates
|
|
||||||
|
|
||||||
copy-images: build-prepare
|
|
||||||
cp -r ./src/public/img/* ./build/public/img
|
|
||||||
|
|
||||||
build: build-js build-css build-font copy-templates copy-images build-go
|
|
||||||
|
|
||||||
watch-js: build-prepare
|
|
||||||
node ./node_modules/.bin/nodemon -e js -w src/public/js/app.js -x 'make build-js' &
|
|
||||||
|
|
||||||
watch-css: build-prepare
|
|
||||||
node ./node_modules/.bin/nodemon -e less -w src/public/less/app.less -x 'make build-css' &
|
|
||||||
|
|
||||||
watch-templates: build-prepare
|
|
||||||
node ./node_modules/.bin/nodemon -e tmpl -w src/templates -x 'make copy-templates' &
|
|
||||||
|
|
||||||
watch-images: build-prepare
|
|
||||||
node ./node_modules/.bin/nodemon -e jpg,png -w src/public/img -x 'make copy-images' &
|
|
||||||
|
|
||||||
watch-go: build-prepare
|
watch-go: build-prepare
|
||||||
CONFIG_FILE="./config.yml" fresh -c fresh.conf
|
CONFIG_FILE="./config.yml" fresh -c fresh.conf
|
||||||
|
|
||||||
watch: watch-js watch-css watch-templates watch-images watch-go
|
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
$(DOCKER_COMPOSE) up -d
|
$(DOCKER_COMPOSE) up -d
|
||||||
sleep 4
|
sleep 4
|
||||||
@ -67,7 +32,7 @@ migration-dev-data: docker migration-schema
|
|||||||
|
|
||||||
migration: migration-schema migration-dev-data
|
migration: migration-schema migration-dev-data
|
||||||
|
|
||||||
dev: docker migration build-font watch
|
dev: docker migration watch-go
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
-rm -r ./build
|
-rm -r ./build
|
||||||
|
73
README.md
73
README.md
@ -1,43 +1,48 @@
|
|||||||
## Install dependencies
|
## Install dependencies
|
||||||
|
|
||||||
Go dependancies:
|
### Install node
|
||||||
|
|
||||||
|
You'll need node v6+, here's a link to install it:
|
||||||
|
https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions
|
||||||
|
|
||||||
|
### Install yarn and dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo npm install -g yarn
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go tools && Dependencies
|
||||||
|
|
||||||
|
As there is no versioning yet, you need to manually go get all the packages
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
go get ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
go tools:
|
||||||
|
|
||||||
```
|
```
|
||||||
go get -v github.com/pilu/fresh
|
go get -v github.com/pilu/fresh
|
||||||
go get -v github.com/mattes/migrate
|
go get -v github.com/mattes/migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
NPM dependencies:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### As there is no versioning yet, you need to manually go get all the packages
|
|
||||||
|
|
||||||
```
|
|
||||||
go get ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
### NOTE: for debian users
|
|
||||||
|
|
||||||
If you use debian the node binary is named nodejs you have to symlink it to node:
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
# ln -s /usr/bin/nodejs /usr/bin/node
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dev
|
## Dev
|
||||||
|
|
||||||
#### Check your config.yml file
|
#### Check your config.yml file
|
||||||
|
|
||||||
#### Run
|
#### Run server
|
||||||
|
|
||||||
```
|
```
|
||||||
make dev
|
make dev
|
||||||
```
|
```
|
||||||
|
|
||||||
To setup the dev env, run server, and auto-reload on file changes
|
#### Run javascript tools
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
## Connect to the database
|
## Connect to the database
|
||||||
|
|
||||||
@ -45,6 +50,12 @@ To setup the dev env, run server, and auto-reload on file changes
|
|||||||
docker run -it --rm --link canape_postgresql_dev:postgres postgres:9.5 psql -h postgres -U test
|
docker run -it --rm --link canape_postgresql_dev:postgres postgres:9.5 psql -h postgres -U test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You'll need to connect to the dev database with this command:
|
||||||
|
|
||||||
|
```
|
||||||
|
\c dev
|
||||||
|
```
|
||||||
|
|
||||||
## Default users
|
## Default users
|
||||||
|
|
||||||
This users are defined with this parameters:
|
This users are defined with this parameters:
|
||||||
@ -67,17 +78,3 @@ make test
|
|||||||
```
|
```
|
||||||
make clean
|
make clean
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
make build
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Watch js and less update, and auto-reload server
|
|
||||||
|
|
||||||
```
|
|
||||||
make watch
|
|
||||||
```
|
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
authorizer:
|
authorizer:
|
||||||
cookie_name: auth
|
secret: my_secret_app_key
|
||||||
cookie_key: mysecretkey
|
|
||||||
pepper: pepper
|
pepper: pepper
|
||||||
cost: 10
|
cost: 10
|
||||||
pgdsn: postgres://test:test@127.0.0.1:5432/dev?sslmode=disable
|
pgdsn: postgres://test:test@127.0.0.1:5432/dev?sslmode=disable
|
||||||
trakttv_client_id: my_trakttv_client_id
|
trakttv_client_id: my_trakttv_client_id
|
||||||
tmdb_api_key: my_tmdb_key
|
tmdb_api_key: my_tmdb_key
|
||||||
listen_port: 3000
|
listen_port: 3000
|
||||||
templates_dir: build/templates
|
|
||||||
public_dir: build/public
|
public_dir: build/public
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"modules": [
|
|
||||||
"bootstrap",
|
|
||||||
"font-awesome"
|
|
||||||
],
|
|
||||||
"dest": "build/public"
|
|
||||||
}
|
|
65
gulpfile.babel.js
Normal file
65
gulpfile.babel.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import gulp from 'gulp';
|
||||||
|
import babel from 'gulp-babel';
|
||||||
|
import less from 'gulp-less';
|
||||||
|
import del from 'del';
|
||||||
|
import webpack from 'webpack-stream';
|
||||||
|
import webpackConfig from './webpack.config.babel';
|
||||||
|
|
||||||
|
const paths = {
|
||||||
|
allSrcJs: 'src/**/*.js?(x)',
|
||||||
|
jsSrc: 'src/public/js/app.js',
|
||||||
|
jsDistDir: 'build/public/js',
|
||||||
|
lessSrc: 'src/public/less/app.less',
|
||||||
|
lessDest: 'build/public/css',
|
||||||
|
fontSrc: [
|
||||||
|
'./node_modules/font-awesome/fonts/*',
|
||||||
|
'./node_modules/bootstrap/fonts/*',
|
||||||
|
],
|
||||||
|
fontDest: 'build/public/fonts',
|
||||||
|
imgSrc: 'src/public/img/*',
|
||||||
|
imgDest: 'build/public/img/',
|
||||||
|
htmlSrc: 'src/public/index.html',
|
||||||
|
htmlDest: 'build/public/',
|
||||||
|
gulpFile: 'gulpfile.babel.js',
|
||||||
|
webpackFile: 'webpack.config.babel.js',
|
||||||
|
};
|
||||||
|
|
||||||
|
gulp.task('less', () =>
|
||||||
|
gulp.src(paths.lessSrc)
|
||||||
|
.pipe(less({
|
||||||
|
paths: [ './node_modules' ]
|
||||||
|
}))
|
||||||
|
.pipe(gulp.dest(paths.lessDest))
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task('fonts', () =>
|
||||||
|
gulp.src(paths.fontSrc)
|
||||||
|
.pipe(gulp.dest(paths.fontDest))
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task('images', () =>
|
||||||
|
gulp.src(paths.imgSrc)
|
||||||
|
.pipe(gulp.dest(paths.imgDest))
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task('html', () =>
|
||||||
|
gulp.src(paths.htmlSrc)
|
||||||
|
.pipe(gulp.dest(paths.htmlDest))
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task('js', () =>
|
||||||
|
gulp.src(paths.jsSrc)
|
||||||
|
.pipe(webpack(webpackConfig))
|
||||||
|
.pipe(gulp.dest(paths.jsDistDir))
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task('main', ['less', 'fonts', 'images', 'html', 'js'])
|
||||||
|
|
||||||
|
gulp.task('watch', () => {
|
||||||
|
gulp.watch(paths.allSrcJs, ['js']);
|
||||||
|
gulp.watch(paths.lessSrc, ['less']);
|
||||||
|
gulp.watch(paths.imgSrc, ['images']);
|
||||||
|
gulp.watch(paths.htmlSrc, ['html']);
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('default', ['watch', 'main']);
|
44
package.json
44
package.json
@ -1,23 +1,45 @@
|
|||||||
{
|
{
|
||||||
"name": "canape",
|
"name": "canape",
|
||||||
"version": "0.0.1",
|
"scripts": {
|
||||||
"description": "``` make dev ```",
|
"start": "gulp"
|
||||||
"main": "index.js",
|
},
|
||||||
"repository": {
|
"babel": {
|
||||||
"type": "git",
|
"presets": [
|
||||||
"url": "ssh://git@gitlab.quimbo.fr:5022/odwrtw/canape-sql.git"
|
"react",
|
||||||
|
"latest"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"babel-polyfill": "^6.16.0",
|
||||||
"bootstrap": "^3.3.6",
|
"bootstrap": "^3.3.6",
|
||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"jquery": "^2.2.4"
|
"history": "^4.4.0",
|
||||||
|
"jquery": "^2.2.4",
|
||||||
|
"jwt-decode": "^2.1.0",
|
||||||
|
"react": "^15.3.2",
|
||||||
|
"react-bootstrap": "^0.30.6",
|
||||||
|
"react-dom": "^15.3.2",
|
||||||
|
"react-redux": "^4.4.6",
|
||||||
|
"react-router": "^3.0.0",
|
||||||
|
"react-router-bootstrap": "^0.23.1",
|
||||||
|
"react-router-redux": "^4.0.7",
|
||||||
|
"redux": "^3.6.0",
|
||||||
|
"redux-auth-wrapper": "^0.9.0",
|
||||||
|
"redux-logger": "^2.7.4",
|
||||||
|
"redux-thunk": "^2.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"browserify": "^13.0.1",
|
"axios": "^0.15.2",
|
||||||
|
"babel": "^6.5.2",
|
||||||
|
"babel-loader": "^6.2.7",
|
||||||
|
"babel-preset-latest": "^6.16.0",
|
||||||
|
"babel-preset-react": "^6.16.0",
|
||||||
|
"del": "^2.2.2",
|
||||||
"fontify": "0.0.2",
|
"fontify": "0.0.2",
|
||||||
|
"gulp": "^3.9.1",
|
||||||
|
"gulp-babel": "^6.1.2",
|
||||||
|
"gulp-less": "^3.3.0",
|
||||||
"less": "^2.7.1",
|
"less": "^2.7.1",
|
||||||
"nodemon": "^1.9.2"
|
"webpack-stream": "^3.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,10 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
jwt "github.com/dgrijalva/jwt-go"
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -13,8 +15,8 @@ var (
|
|||||||
ErrInvalidPassword = fmt.Errorf("Invalid password")
|
ErrInvalidPassword = fmt.Errorf("Invalid password")
|
||||||
// ErrInvalidSecret returned when cookie's secret is don't match
|
// ErrInvalidSecret returned when cookie's secret is don't match
|
||||||
ErrInvalidSecret = fmt.Errorf("Invalid secret")
|
ErrInvalidSecret = fmt.Errorf("Invalid secret")
|
||||||
// ErrCorrupted returned when session have been corrupted
|
// ErrInvalidToken returned when the jwt token is invalid
|
||||||
ErrCorrupted = fmt.Errorf("Corrupted session")
|
ErrInvalidToken = fmt.Errorf("Invalid token")
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserBackend interface for user backend
|
// UserBackend interface for user backend
|
||||||
@ -27,6 +29,7 @@ type User interface {
|
|||||||
GetName() string
|
GetName() string
|
||||||
GetHash() string
|
GetHash() string
|
||||||
HasRole(string) bool
|
HasRole(string) bool
|
||||||
|
IsAdmin() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authorizer handle sesssion
|
// Authorizer handle sesssion
|
||||||
@ -37,10 +40,9 @@ type Authorizer struct {
|
|||||||
// Params for Authorizer creation
|
// Params for Authorizer creation
|
||||||
type Params struct {
|
type Params struct {
|
||||||
Backend UserBackend
|
Backend UserBackend
|
||||||
Cookiejar *sessions.CookieStore
|
|
||||||
CookieName string
|
|
||||||
Pepper string
|
Pepper string
|
||||||
Cost int
|
Cost int
|
||||||
|
Secret string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New Authorizer pepper is like a salt but not stored in database,
|
// New Authorizer pepper is like a salt but not stored in database,
|
||||||
@ -60,112 +62,56 @@ func (a *Authorizer) GenHash(password string) (string, error) {
|
|||||||
return string(b), nil
|
return string(b), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login cheks password and updates cookie info
|
// Login cheks password and creates a jwt token
|
||||||
func (a *Authorizer) Login(rw http.ResponseWriter, req *http.Request, username, password string) error {
|
func (a *Authorizer) Login(rw http.ResponseWriter, req *http.Request, username, password string) (User, error) {
|
||||||
cookie, err := a.Cookiejar.Get(req, a.CookieName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := a.Backend.Get(username)
|
u, err := a.Backend.Get(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compare the password
|
||||||
err = bcrypt.CompareHashAndPassword([]byte(u.GetHash()), []byte(password+a.Pepper))
|
err = bcrypt.CompareHashAndPassword([]byte(u.GetHash()), []byte(password+a.Pepper))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrInvalidPassword
|
return nil, ErrInvalidPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie.Values["username"] = username
|
return u, nil
|
||||||
|
}
|
||||||
// genereate secret
|
|
||||||
b, err := bcrypt.GenerateFromPassword([]byte(u.GetHash()), a.Cost)
|
// CurrentUser returns the logged in username from session and verifies the token
|
||||||
if err != nil {
|
func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (User, error) {
|
||||||
return err
|
h := req.Header.Get("Authorization")
|
||||||
}
|
// No user logged
|
||||||
cookie.Values["secret"] = string(b)
|
if h == "" {
|
||||||
|
return nil, nil
|
||||||
err = cookie.Save(req, rw)
|
}
|
||||||
if err != nil {
|
|
||||||
return err
|
// Get the token from the header
|
||||||
}
|
tokenStr := strings.Replace(h, "Bearer ", "", -1)
|
||||||
return nil
|
|
||||||
}
|
// Keyfunc to decode the token
|
||||||
|
var keyfunc jwt.Keyfunc = func(token *jwt.Token) (interface{}, error) {
|
||||||
// RegenSecret update secret in cookie with user info, usefull when updating password
|
return []byte(a.Secret), nil
|
||||||
func (a *Authorizer) RegenSecret(user User, w http.ResponseWriter, r *http.Request) error {
|
}
|
||||||
cookie, err := a.Cookiejar.Get(r, a.CookieName)
|
|
||||||
if err != nil {
|
var tokenClaims struct {
|
||||||
return err
|
Username string `json:"username"`
|
||||||
}
|
jwt.StandardClaims
|
||||||
// genereate secret
|
}
|
||||||
b, err := bcrypt.GenerateFromPassword([]byte(user.GetHash()), a.Cost)
|
token, err := jwt.ParseWithClaims(tokenStr, &tokenClaims, keyfunc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
cookie.Values["secret"] = string(b)
|
|
||||||
|
// Check the token validity
|
||||||
err = cookie.Save(r, w)
|
if !token.Valid {
|
||||||
if err != nil {
|
return nil, ErrInvalidToken
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
return nil
|
// Get the user
|
||||||
}
|
u, err := a.Backend.Get(tokenClaims.Username)
|
||||||
|
if err != nil {
|
||||||
// Logout remove cookie info
|
return nil, err
|
||||||
func (a *Authorizer) Logout(rw http.ResponseWriter, req *http.Request) error {
|
|
||||||
cookie, err := a.Cookiejar.Get(req, a.CookieName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cookie.Values["username"] = nil
|
|
||||||
cookie.Values["secret"] = nil
|
|
||||||
cookie.Options.MaxAge = -1 // kill the cookie
|
|
||||||
err = cookie.Save(req, rw)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CurrentUser returns the logged in username from session
|
|
||||||
func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (User, error) {
|
|
||||||
cookie, err := a.Cookiejar.Get(req, a.CookieName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if cookie.IsNew {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
usernameTmp := cookie.Values["username"]
|
|
||||||
if usernameTmp == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
username, ok := usernameTmp.(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrCorrupted
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := a.Backend.Get(username)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check secret
|
|
||||||
hash := u.GetHash()
|
|
||||||
secretTmp := cookie.Values["secret"]
|
|
||||||
if secretTmp == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
secret, ok := secretTmp.(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrCorrupted
|
|
||||||
}
|
|
||||||
err = bcrypt.CompareHashAndPassword([]byte(secret), []byte(hash))
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrInvalidSecret
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return u, nil
|
return u, nil
|
||||||
|
@ -1,199 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/cookiejar"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/kr/pretty"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
pepper = "polp"
|
|
||||||
ckey = "plop"
|
|
||||||
cookieName = "auth"
|
|
||||||
cost = 10
|
|
||||||
|
|
||||||
username = "plop"
|
|
||||||
password = "ploppwd"
|
|
||||||
hash = "$2a$10$eVye8xbs6nj4TWnlTmifRuBsAU3F2hkxEcFz9WXdYjUuE6uKLVuzK"
|
|
||||||
)
|
|
||||||
|
|
||||||
type user struct {
|
|
||||||
username string
|
|
||||||
password string
|
|
||||||
hash string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *user) GetHash() string {
|
|
||||||
return u.hash
|
|
||||||
}
|
|
||||||
|
|
||||||
type Backend struct {
|
|
||||||
user *user
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Backend) Get(username string) (User, error) {
|
|
||||||
if username == b.user.username {
|
|
||||||
return b.user, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("invalid username")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBackend() *Backend {
|
|
||||||
return &Backend{
|
|
||||||
user: &user{
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
hash: hash,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func login(w http.ResponseWriter, r *http.Request) {
|
|
||||||
a := New(getBackend(), pepper, cookieName, ckey, cost)
|
|
||||||
err := a.Login(w, r, username, password)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(w, "%s", err)
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
|
|
||||||
}
|
|
||||||
func logout(w http.ResponseWriter, r *http.Request) {
|
|
||||||
a := New(getBackend(), pepper, cookieName, ckey, cost)
|
|
||||||
err := a.Logout(w, r)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(w, "%s", err)
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
func check(w http.ResponseWriter, r *http.Request) {
|
|
||||||
a := New(getBackend(), pepper, cookieName, key, cost)
|
|
||||||
u, err := a.CurrentUser(w, r)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
fmt.Fprintf(w, "%s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
if u != nil {
|
|
||||||
usr, ok := u.(*user)
|
|
||||||
if !ok {
|
|
||||||
fmt.Fprintf(w, "Invalid user type")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprintf(w, "%s", usr.username)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlers() *mux.Router {
|
|
||||||
r := mux.NewRouter()
|
|
||||||
r.HandleFunc("/login", login).Methods("GET")
|
|
||||||
r.HandleFunc("/logout", logout).Methods("GET")
|
|
||||||
r.HandleFunc("/check", check).Methods("GET")
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuth(t *testing.T) {
|
|
||||||
ts := httptest.NewServer(handlers())
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
cookieJar, _ := cookiejar.New(nil)
|
|
||||||
client := &http.Client{
|
|
||||||
Jar: cookieJar,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check no user logged in =
|
|
||||||
res, err := client.Get(ts.URL + "/check")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
t.Fatal(body)
|
|
||||||
}
|
|
||||||
if string(body) != "" {
|
|
||||||
t.Fatalf("No user logged in expected but found: %s", body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login
|
|
||||||
res, err = client.Get(ts.URL + "/login")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err = ioutil.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
t.Fatal(string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks we are logged in
|
|
||||||
res, err = client.Get(ts.URL + "/check")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err = ioutil.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
pretty.Println(res.StatusCode)
|
|
||||||
t.Fatal(body)
|
|
||||||
}
|
|
||||||
if string(body) != username {
|
|
||||||
t.Fatalf("We expect be logged in as %s but we got: %s", username, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout
|
|
||||||
res, err = client.Get(ts.URL + "/logout")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err = ioutil.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
t.Fatal(string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check no username logged in anymore
|
|
||||||
res, err = client.Get(ts.URL + "/check")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err = ioutil.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
t.Fatal(body)
|
|
||||||
}
|
|
||||||
if string(body) != "" {
|
|
||||||
t.Fatalf("No user logged in expected but found: %s", body)
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,11 +2,8 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/data"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -27,13 +24,12 @@ func NewMiddleware(authorizer *Authorizer, log *logrus.Entry) *Middleware {
|
|||||||
func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||||
user, err := m.authorizer.CurrentUser(w, r)
|
user, err := m.authorizer.CurrentUser(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user != nil {
|
if user != nil {
|
||||||
name := user.GetName()
|
m.log.Debugf("setting user %s in the context", user.GetName())
|
||||||
m.log.Debugf("setting user %s in the context", name)
|
|
||||||
data.SetData(r, "user", user)
|
|
||||||
} else {
|
} else {
|
||||||
m.log.Debugf("got a nil user in the context")
|
m.log.Debugf("got a nil user in the context")
|
||||||
}
|
}
|
||||||
@ -49,16 +45,14 @@ type MiddlewareRole struct {
|
|||||||
authorizer *Authorizer
|
authorizer *Authorizer
|
||||||
log *logrus.Entry
|
log *logrus.Entry
|
||||||
role string
|
role string
|
||||||
loginPageGetter func() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMiddlewareRole returns a new MiddlewareRole
|
// NewMiddlewareRole returns a new MiddlewareRole
|
||||||
func NewMiddlewareRole(authorizer *Authorizer, log *logrus.Entry, loginPageGetter func() string, role string) *MiddlewareRole {
|
func NewMiddlewareRole(authorizer *Authorizer, log *logrus.Entry, role string) *MiddlewareRole {
|
||||||
return &MiddlewareRole{
|
return &MiddlewareRole{
|
||||||
authorizer: authorizer,
|
authorizer: authorizer,
|
||||||
log: log.WithField("middleware", "role"),
|
log: log.WithField("middleware", "role"),
|
||||||
role: role,
|
role: role,
|
||||||
loginPageGetter: loginPageGetter,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,16 +66,8 @@ func (m *MiddlewareRole) ServeHTTP(w http.ResponseWriter, r *http.Request, next
|
|||||||
m.log.Debug("user doesn't have the role")
|
m.log.Debug("user doesn't have the role")
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie, err := m.authorizer.Cookiejar.Get(r, "rlogin")
|
// return unauthorized
|
||||||
if err != nil {
|
http.Error(w, "Invalid user role", http.StatusUnauthorized)
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
cookie.Values["redirect"] = r.URL.Path
|
|
||||||
err = cookie.Save(r, w)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
http.Redirect(w, r, m.loginPageGetter(), http.StatusTemporaryRedirect)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,30 +76,6 @@ func (m *MiddlewareRole) ServeHTTP(w http.ResponseWriter, r *http.Request, next
|
|||||||
next(w, r)
|
next(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPostLoginRedirect returns the location of the page requested before the
|
|
||||||
// users was redirected to the login page
|
|
||||||
func GetPostLoginRedirect(a *Authorizer, w http.ResponseWriter, r *http.Request) (string, error) {
|
|
||||||
cookie, err := a.Cookiejar.Get(r, "rlogin")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
val := cookie.Values["redirect"]
|
|
||||||
if val == nil {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
path, ok := val.(string)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("invalid redirect type")
|
|
||||||
}
|
|
||||||
cookie.Values["rlogin"] = ""
|
|
||||||
err = cookie.Save(r, w)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return path, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCurrentUser gets the current user from the request context
|
// GetCurrentUser gets the current user from the request context
|
||||||
func GetCurrentUser(r *http.Request, log *logrus.Entry) User {
|
func GetCurrentUser(r *http.Request, log *logrus.Entry) User {
|
||||||
log.Debug("getting user from context")
|
log.Debug("getting user from context")
|
||||||
|
@ -15,7 +15,6 @@ type Config struct {
|
|||||||
PGDSN string `yaml:"pgdsn"`
|
PGDSN string `yaml:"pgdsn"`
|
||||||
Port string `yaml:"listen_port"`
|
Port string `yaml:"listen_port"`
|
||||||
TraktTVClientID string `yaml:"trakttv_client_id"`
|
TraktTVClientID string `yaml:"trakttv_client_id"`
|
||||||
TemplatesDir string `yaml:"templates_dir"`
|
|
||||||
PublicDir string `yaml:"public_dir"`
|
PublicDir string `yaml:"public_dir"`
|
||||||
|
|
||||||
// TODO improve the detailers configurations
|
// TODO improve the detailers configurations
|
||||||
@ -24,10 +23,9 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizerConfig struct {
|
type AuthorizerConfig struct {
|
||||||
CookieName string `yaml:"cookie_name"`
|
|
||||||
Key string `yaml:"cookie_key"`
|
|
||||||
Pepper string `yaml:"pepper"`
|
Pepper string `yaml:"pepper"`
|
||||||
Cost int `yaml:"cost"`
|
Cost int `yaml:"cost"`
|
||||||
|
Secret string `yaml:"secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load(path string) (*Config, error) {
|
func Load(path string) (*Config, error) {
|
||||||
|
@ -2,6 +2,6 @@ package config
|
|||||||
|
|
||||||
// UserPolochon is polochon access parameter for a user
|
// UserPolochon is polochon access parameter for a user
|
||||||
type UserPolochon struct {
|
type UserPolochon struct {
|
||||||
URL string
|
URL string `json:"url"`
|
||||||
Token string
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
package data
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type key int
|
|
||||||
|
|
||||||
// dKey is key for access to response data in context
|
|
||||||
const dKey key = 0
|
|
||||||
|
|
||||||
// GetAllData return response's data
|
|
||||||
func GetAllData(r *http.Request) map[string]interface{} {
|
|
||||||
data := GetData(r, "data")
|
|
||||||
if data == nil {
|
|
||||||
data = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
mapData, ok := data.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
fmt.Printf("something wrong with data")
|
|
||||||
}
|
|
||||||
// log.Printf("got all data %+v", mapData)
|
|
||||||
return mapData
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetData sets some response's data for access in template
|
|
||||||
func SetData(r *http.Request, key string, val interface{}) {
|
|
||||||
allData := GetAllData(r)
|
|
||||||
allData[key] = val
|
|
||||||
|
|
||||||
ctx := context.WithValue(r.Context(), "data", allData)
|
|
||||||
*r = *r.WithContext(ctx)
|
|
||||||
allData = GetAllData(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetData gets some response's data for access in template
|
|
||||||
func GetData(r *http.Request, key string) interface{} {
|
|
||||||
return r.Context().Value(key)
|
|
||||||
}
|
|
@ -5,8 +5,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/odwrtw/papi"
|
"github.com/odwrtw/papi"
|
||||||
polochon "github.com/odwrtw/polochon/lib"
|
polochon "github.com/odwrtw/polochon/lib"
|
||||||
@ -15,19 +13,13 @@ import (
|
|||||||
|
|
||||||
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth"
|
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth"
|
||||||
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config"
|
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config"
|
||||||
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/data"
|
|
||||||
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users"
|
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users"
|
||||||
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
|
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrPolochonUnavailable is an error returned if the polochon server is not available
|
||||||
var ErrPolochonUnavailable = fmt.Errorf("Invalid polochon address")
|
var ErrPolochonUnavailable = fmt.Errorf("Invalid polochon address")
|
||||||
|
|
||||||
type SortByTitle []*Movie
|
|
||||||
|
|
||||||
func (a SortByTitle) Len() int { return len(a) }
|
|
||||||
func (a SortByTitle) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
||||||
func (a SortByTitle) Less(i, j int) bool { return a[i].Title < a[j].Title }
|
|
||||||
|
|
||||||
func getPolochonMovies(user *users.User) ([]*Movie, error) {
|
func getPolochonMovies(user *users.User) ([]*Movie, error) {
|
||||||
movies := []*Movie{}
|
movies := []*Movie{}
|
||||||
|
|
||||||
@ -72,61 +64,14 @@ func getPolochonMovies(user *users.User) ([]*Movie, error) {
|
|||||||
|
|
||||||
func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error {
|
func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error {
|
||||||
v := auth.GetCurrentUser(r, env.Log)
|
v := auth.GetCurrentUser(r, env.Log)
|
||||||
params := struct {
|
|
||||||
Sort string
|
|
||||||
Order string
|
|
||||||
Start int
|
|
||||||
Limit int
|
|
||||||
}{
|
|
||||||
Sort: "title",
|
|
||||||
Order: "asc",
|
|
||||||
Start: 0,
|
|
||||||
Limit: 50,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.ParseForm()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if sort := r.FormValue("sort"); sort != "" {
|
|
||||||
params.Sort = sort
|
|
||||||
}
|
|
||||||
|
|
||||||
if order := r.FormValue("order"); order != "" {
|
|
||||||
params.Order = order
|
|
||||||
}
|
|
||||||
if start := r.FormValue("start"); start != "" {
|
|
||||||
n, err := strconv.Atoi(start)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
params.Start = n
|
|
||||||
}
|
|
||||||
if limit := r.FormValue("limit"); limit != "" {
|
|
||||||
n, err := strconv.Atoi(limit)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
params.Limit = n
|
|
||||||
}
|
|
||||||
|
|
||||||
user, ok := v.(*users.User)
|
user, ok := v.(*users.User)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("invalid user type")
|
return fmt.Errorf("invalid user type")
|
||||||
}
|
}
|
||||||
data.SetData(r, "user", user)
|
|
||||||
|
|
||||||
movies, err := getPolochonMovies(user)
|
movies, err := getPolochonMovies(user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Catch network error for accessing specified polochon address
|
return env.RenderError(w, err)
|
||||||
if err == ErrPolochonUnavailable {
|
|
||||||
data.SetData(r, "error", "Invalid address")
|
|
||||||
return env.Rends(w, r, "movies/library")
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var polochonConfig config.UserPolochon
|
var polochonConfig config.UserPolochon
|
||||||
@ -148,30 +93,10 @@ func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var smovies sort.Interface
|
return env.RenderJSON(w, movies)
|
||||||
|
|
||||||
switch params.Sort {
|
|
||||||
case "title":
|
|
||||||
smovies = SortByTitle(movies)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid sort param")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch params.Order {
|
|
||||||
case "asc":
|
|
||||||
break
|
|
||||||
case "desc":
|
|
||||||
smovies = sort.Reverse(smovies)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid order param")
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(smovies)
|
|
||||||
|
|
||||||
data.SetData(r, "movies", movies[params.Start:params.Start+params.Limit])
|
|
||||||
return env.Rends(w, r, "movies/library")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExplorePopular returns the popular movies
|
||||||
func ExplorePopular(env *web.Env, w http.ResponseWriter, r *http.Request) error {
|
func ExplorePopular(env *web.Env, w http.ResponseWriter, r *http.Request) error {
|
||||||
log := env.Log.WithField("function", "movies.ExplorePopular")
|
log := env.Log.WithField("function", "movies.ExplorePopular")
|
||||||
|
|
||||||
@ -206,7 +131,6 @@ func ExplorePopular(env *web.Env, w http.ResponseWriter, r *http.Request) error
|
|||||||
movies = append(movies, movie)
|
movies = append(movies, movie)
|
||||||
ids = append(ids, m.IDs.ImDB)
|
ids = append(ids, m.IDs.ImDB)
|
||||||
}
|
}
|
||||||
data.SetData(r, "movies", movies)
|
|
||||||
|
|
||||||
return env.Rends(w, r, "movies/library")
|
return env.RenderJSON(w, movies)
|
||||||
}
|
}
|
||||||
|
@ -1,134 +1,102 @@
|
|||||||
package users
|
package users
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/Schema"
|
jwt "github.com/dgrijalva/jwt-go"
|
||||||
"github.com/kr/pretty"
|
|
||||||
|
|
||||||
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth"
|
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth"
|
||||||
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config"
|
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config"
|
||||||
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/data"
|
|
||||||
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
|
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoginGETHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return e.Rends(w, r, "users/login")
|
|
||||||
}
|
|
||||||
|
|
||||||
func SignupGETHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return e.Rends(w, r, "users/signup")
|
|
||||||
}
|
|
||||||
|
|
||||||
func SignupPOSTHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
|
func SignupPOSTHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
|
||||||
type newForm struct {
|
var data struct {
|
||||||
Username string
|
Username string `json:"username"`
|
||||||
Password string
|
Password string `json:"password"`
|
||||||
PasswordVerify string
|
PasswordConfirm string `json:"password_confirm"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
e.Log.Debugf("creating new user ...")
|
e.Log.Debugf("creating new user ...")
|
||||||
|
|
||||||
err := r.ParseForm()
|
if data.Password == "" && data.PasswordConfirm != "" {
|
||||||
if err != nil {
|
return e.RenderError(w, fmt.Errorf("Empty password"))
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
form := new(newForm)
|
if data.Password != data.PasswordConfirm {
|
||||||
decoder := schema.NewDecoder()
|
return e.RenderError(w, fmt.Errorf("Passwords missmatch"))
|
||||||
err = decoder.Decode(form, r.PostForm)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user := User{
|
user := User{Name: data.Username}
|
||||||
Name: form.Username,
|
|
||||||
}
|
var err error
|
||||||
if form.Password != "" || form.PasswordVerify != "" {
|
user.Hash, err = e.Auth.GenHash(data.Password)
|
||||||
if form.Password != form.PasswordVerify {
|
|
||||||
data.SetData(r, "Errors", "Password mismatch")
|
|
||||||
return e.Rends(w, r, "users/signup")
|
|
||||||
}
|
|
||||||
user.Hash, err = e.Auth.GenHash(form.Password)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
err = user.NewConfig()
|
err = user.NewConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = user.Add(e.Database)
|
if err = user.Add(e.Database); err != nil {
|
||||||
if err != nil {
|
|
||||||
pretty.Println(err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
e.Log.Debugf("new user %s created ...", form.Username)
|
e.Log.Debugf("new user %s created ...", data.Username)
|
||||||
|
|
||||||
err = e.Auth.Login(w, r, form.Username, form.Password)
|
return e.RenderOK(w, "User created")
|
||||||
if err != nil {
|
|
||||||
if err == auth.ErrInvalidPassword || err == ErrUnknownUser {
|
|
||||||
data.SetData(r, "Errors", "Error invalid user or password")
|
|
||||||
return e.Rends(w, r, "users/signup")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoginPOSTHandler handles the login proccess
|
||||||
func LoginPOSTHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
|
func LoginPOSTHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
|
||||||
type loginForm struct {
|
var data struct {
|
||||||
Username string
|
Username string `json:"username"`
|
||||||
Password string
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := r.ParseForm()
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
form := new(loginForm)
|
user, err := e.Auth.Login(w, r, data.Username, data.Password)
|
||||||
decoder := schema.NewDecoder()
|
|
||||||
err = decoder.Decode(form, r.PostForm)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = e.Auth.Login(w, r, form.Username, form.Password)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == auth.ErrInvalidPassword || err == ErrUnknownUser {
|
if err == auth.ErrInvalidPassword || err == ErrUnknownUser {
|
||||||
data.SetData(r, "Errors", "Error invalid user or password")
|
return e.RenderError(w, fmt.Errorf("Error invalid user or password"))
|
||||||
return e.Rends(w, r, "users/login")
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
e.Log.Debug("logged")
|
e.Log.Debugf("logged %s", user.GetName())
|
||||||
|
|
||||||
path, err := auth.GetPostLoginRedirect(e.Auth, w, r)
|
// Create a jwt token
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
// Not before
|
||||||
|
"nbf": time.Now().Unix(),
|
||||||
|
// Issued at
|
||||||
|
"iat": time.Now().Unix(),
|
||||||
|
// Private claims
|
||||||
|
"username": user.GetName(),
|
||||||
|
"isAdmin": user.IsAdmin(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sign the token
|
||||||
|
ss, err := token.SignedString([]byte(e.Auth.Secret))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Log.Debugf("redirecting to %s", path)
|
var out = struct {
|
||||||
if path != "" {
|
Token string `json:"token"`
|
||||||
http.Redirect(w, r, path, http.StatusTemporaryRedirect)
|
}{
|
||||||
return nil
|
Token: ss,
|
||||||
}
|
}
|
||||||
e.Log.Debugf("got no path, redirecting to /")
|
|
||||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogoutHandler just logout
|
return e.RenderJSON(w, out)
|
||||||
func LogoutHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
|
|
||||||
e.Auth.Logout(w, r)
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DetailsHandler show user details
|
// DetailsHandler show user details
|
||||||
@ -145,8 +113,7 @@ func DetailsHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
data.SetData(r, "polochon", polochonConfig)
|
return e.RenderJSON(w, polochonConfig)
|
||||||
return e.Rends(w, r, "users/details")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditHandler allow editing user info and configuration
|
// EditHandler allow editing user info and configuration
|
||||||
@ -156,70 +123,44 @@ func EditHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("invalid user type")
|
return fmt.Errorf("invalid user type")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
PolochonURL string `json:"polochon_url"`
|
||||||
|
PolochonToken string `json:"polochon_token"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
PasswordConfirm string `json:"password_confirm"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Password == "" && data.PasswordConfirm != "" {
|
||||||
|
if data.Password != data.PasswordConfirm {
|
||||||
|
return e.RenderError(w, fmt.Errorf("Passwords empty or missmatch"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the user config
|
||||||
|
var err error
|
||||||
|
user.Hash, err = e.Auth.GenHash(data.Password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.Update(e.Database); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the polochon config
|
||||||
var polochonConfig config.UserPolochon
|
var polochonConfig config.UserPolochon
|
||||||
err := user.GetConfig("polochon", &polochonConfig)
|
if err := user.GetConfig("polochon", &polochonConfig); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
|
}
|
||||||
|
polochonConfig.URL = data.PolochonURL
|
||||||
|
polochonConfig.Token = data.PolochonToken
|
||||||
|
if err := user.SetConfig("polochon", polochonConfig); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Method == "GET" {
|
return e.RenderOK(w, "user updated")
|
||||||
data.SetData(r, "polochon", polochonConfig)
|
|
||||||
return e.Rends(w, r, "users/edit")
|
|
||||||
}
|
|
||||||
|
|
||||||
type editForm struct {
|
|
||||||
PolochonURL string
|
|
||||||
PolochonToken string
|
|
||||||
Password string
|
|
||||||
PasswordVerify string
|
|
||||||
}
|
|
||||||
|
|
||||||
err = r.ParseForm()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
form := new(editForm)
|
|
||||||
decoder := schema.NewDecoder()
|
|
||||||
err = decoder.Decode(form, r.PostForm)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
polochonConfig.URL = form.PolochonURL
|
|
||||||
polochonConfig.Token = form.PolochonToken
|
|
||||||
|
|
||||||
err = user.SetConfig("polochon", polochonConfig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if form.Password != "" || form.PasswordVerify != "" {
|
|
||||||
if form.Password != form.PasswordVerify {
|
|
||||||
// TODO: manage form error
|
|
||||||
}
|
|
||||||
user.Hash, err = e.Auth.GenHash(form.Password)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = user.Update(e.Database)
|
|
||||||
if err != nil {
|
|
||||||
pretty.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = e.Auth.RegenSecret(user, w, r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
url, err := e.GetURL("users.details")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
@ -204,3 +204,7 @@ func (u *User) HasRole(role string) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) IsAdmin() bool {
|
||||||
|
return u.HasRole(AdminRole)
|
||||||
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth"
|
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth"
|
||||||
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config"
|
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config"
|
||||||
|
|
||||||
@ -34,28 +32,14 @@ type EnvParams struct {
|
|||||||
|
|
||||||
// NewEnv returns a new *Env
|
// NewEnv returns a new *Env
|
||||||
func NewEnv(p EnvParams) *Env {
|
func NewEnv(p EnvParams) *Env {
|
||||||
e := &Env{
|
return &Env{
|
||||||
Database: p.Database,
|
Database: p.Database,
|
||||||
Log: p.Log,
|
Log: p.Log,
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
Auth: p.Auth,
|
Auth: p.Auth,
|
||||||
Config: p.Config,
|
Config: p.Config,
|
||||||
|
Render: render.New(),
|
||||||
}
|
}
|
||||||
|
|
||||||
tmplFuncs = append(tmplFuncs, map[string]interface{}{
|
|
||||||
"URL": func(name string, pairs ...string) (string, error) {
|
|
||||||
return e.GetURL(name, pairs...)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
e.Render = render.New(render.Options{
|
|
||||||
Directory: p.Config.TemplatesDir,
|
|
||||||
Layout: "layout",
|
|
||||||
Funcs: tmplFuncs,
|
|
||||||
DisableHTTPErrorRendering: true,
|
|
||||||
RequirePartials: true,
|
|
||||||
})
|
|
||||||
return e
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Route struct {
|
type Route struct {
|
||||||
@ -76,7 +60,7 @@ func (r *Route) Methods(methods ...string) *Route {
|
|||||||
func (r *Route) WithRole(role string) *Route {
|
func (r *Route) WithRole(role string) *Route {
|
||||||
handler := r.mRoute.GetHandler()
|
handler := r.mRoute.GetHandler()
|
||||||
newHandler := negroni.New(
|
newHandler := negroni.New(
|
||||||
auth.NewMiddlewareRole(r.env.Auth, r.env.Log, r.env.GetLoginRouteGetter(), role),
|
auth.NewMiddlewareRole(r.env.Auth, r.env.Log, role),
|
||||||
negroni.Wrap(handler))
|
negroni.Wrap(handler))
|
||||||
r.mRoute.Handler(newHandler)
|
r.mRoute.Handler(newHandler)
|
||||||
return r
|
return r
|
||||||
@ -90,39 +74,3 @@ func (e *Env) Handle(route string, H HandlerFunc) *Route {
|
|||||||
mRoute: mRoute,
|
mRoute: mRoute,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetURL returns URL string from URL name and params
|
|
||||||
// Usefull for redirection and templates
|
|
||||||
func (e *Env) GetURL(name string, pairs ...string) (string, error) {
|
|
||||||
route := e.Router.Get(name)
|
|
||||||
if route == nil {
|
|
||||||
return "", fmt.Errorf("No route find for the given name: %s", name)
|
|
||||||
}
|
|
||||||
URL, err := route.URL(pairs...)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return URL.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLoginRoute sets the route name of login page for further use
|
|
||||||
// with GetLoginRouteGetter
|
|
||||||
func (e *Env) SetLoginRoute(name string) error {
|
|
||||||
route, err := e.GetURL(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
e.loginRoute = route
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLoginRouteGetter allow some code parts like middleware access
|
|
||||||
// to login route name
|
|
||||||
func (e *Env) GetLoginRouteGetter() func() string {
|
|
||||||
return func() string {
|
|
||||||
if e.loginRoute == "" {
|
|
||||||
panic("Env: login route not set")
|
|
||||||
}
|
|
||||||
return e.loginRoute
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
36
src/internal/web/render.go
Normal file
36
src/internal/web/render.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// RenderError renders an error
|
||||||
|
func (e *Env) RenderError(w http.ResponseWriter, err error) error {
|
||||||
|
return e.render(w, "error", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderOK renders a message
|
||||||
|
func (e *Env) RenderOK(w http.ResponseWriter, message string) error {
|
||||||
|
return e.render(w, "ok", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Env) render(w http.ResponseWriter, status, message string) error {
|
||||||
|
var out = struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}{
|
||||||
|
Status: status,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
return e.Render.JSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderJSON renders the data in JSON format
|
||||||
|
func (e *Env) RenderJSON(w http.ResponseWriter, data interface{}) error {
|
||||||
|
var out = struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}{
|
||||||
|
Status: "ok",
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
return e.Render.JSON(w, http.StatusOK, out)
|
||||||
|
}
|
@ -1,48 +0,0 @@
|
|||||||
package web
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/data"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TmplFuncs handles global template functions
|
|
||||||
var tmplFuncs = []template.FuncMap{
|
|
||||||
map[string]interface{}{
|
|
||||||
"safeURL": func(s string) template.URL {
|
|
||||||
return template.URL(s)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddTmplFunc adds a template function
|
|
||||||
func AddTmplFunc(name string, f interface{}) error {
|
|
||||||
tmplFuncs = append(tmplFuncs, map[string]interface{}{
|
|
||||||
name: f,
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TemplateData represents object passed to template renderer
|
|
||||||
type TemplateData struct {
|
|
||||||
Route string
|
|
||||||
Data map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rends a view
|
|
||||||
func (e *Env) Rends(w http.ResponseWriter, r *http.Request, template string) error {
|
|
||||||
if r.Header.Get("Accept") == "application/json" {
|
|
||||||
return e.Render.JSON(w, http.StatusOK, TemplateData{
|
|
||||||
Route: mux.CurrentRoute(r).GetName(),
|
|
||||||
Data: data.GetAllData(r),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.Render.HTML(w, http.StatusOK, template, TemplateData{
|
|
||||||
Route: mux.CurrentRoute(r).GetName(),
|
|
||||||
Data: data.GetAllData(r),
|
|
||||||
})
|
|
||||||
}
|
|
27
src/main.go
27
src/main.go
@ -11,9 +11,9 @@ import (
|
|||||||
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
|
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
|
"github.com/phyber/negroni-gzip/gzip"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -54,9 +54,8 @@ func main() {
|
|||||||
authParams := auth.Params{
|
authParams := auth.Params{
|
||||||
Backend: uBackend,
|
Backend: uBackend,
|
||||||
Pepper: cf.Authorizer.Pepper,
|
Pepper: cf.Authorizer.Pepper,
|
||||||
CookieName: cf.Authorizer.CookieName,
|
|
||||||
Cookiejar: sessions.NewCookieStore([]byte(cf.Authorizer.Key)),
|
|
||||||
Cost: cf.Authorizer.Cost,
|
Cost: cf.Authorizer.Cost,
|
||||||
|
Secret: cf.Authorizer.Secret,
|
||||||
}
|
}
|
||||||
authorizer := auth.New(authParams)
|
authorizer := auth.New(authParams)
|
||||||
|
|
||||||
@ -69,26 +68,18 @@ func main() {
|
|||||||
|
|
||||||
authMiddleware := auth.NewMiddleware(env.Auth, log)
|
authMiddleware := auth.NewMiddleware(env.Auth, log)
|
||||||
|
|
||||||
env.Handle("/", movies.ExplorePopular).Name("movies.home")
|
env.Handle("/users/login", users.LoginPOSTHandler).Methods("POST")
|
||||||
env.Handle("/users/login", users.LoginGETHandler).Name("users.login").Methods("GET")
|
env.Handle("/users/signup", users.SignupPOSTHandler).Methods("POST")
|
||||||
env.Handle("/users/login", users.LoginPOSTHandler).Name("users.login").Methods("POST")
|
env.Handle("/users/details", users.DetailsHandler).WithRole(users.UserRole)
|
||||||
env.Handle("/users/signup", users.SignupGETHandler).Name("users.signup").Methods("GET")
|
env.Handle("/users/edit", users.EditHandler).WithRole(users.UserRole)
|
||||||
env.Handle("/users/signup", users.SignupPOSTHandler).Name("users.signup").Methods("POST")
|
|
||||||
env.Handle("/users/logout", users.LogoutHandler).Name("users.logout")
|
|
||||||
env.Handle("/users/details", users.DetailsHandler).Name("users.details").WithRole(users.UserRole)
|
|
||||||
env.Handle("/users/edit", users.EditHandler).Name("users.edit").WithRole(users.UserRole)
|
|
||||||
|
|
||||||
env.Handle("/movies/polochon", movies.FromPolochon).Name("movies.polochon").WithRole(users.UserRole)
|
env.Handle("/movies/polochon", movies.FromPolochon).WithRole(users.UserRole)
|
||||||
env.Handle("/movies/explore/popular", movies.ExplorePopular).Name("movies.explore.popular").WithRole(users.UserRole)
|
env.Handle("/movies/explore/popular", movies.ExplorePopular).WithRole(users.UserRole)
|
||||||
|
|
||||||
err = env.SetLoginRoute("users.login")
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
n := negroni.Classic()
|
n := negroni.Classic()
|
||||||
n.Use(authMiddleware)
|
n.Use(authMiddleware)
|
||||||
n.Use(negroni.NewStatic(http.Dir(cf.PublicDir)))
|
n.Use(negroni.NewStatic(http.Dir(cf.PublicDir)))
|
||||||
|
n.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||||
n.UseHandler(env.Router)
|
n.UseHandler(env.Router)
|
||||||
n.Run(":" + cf.Port)
|
n.Run(":" + cf.Port)
|
||||||
}
|
}
|
||||||
|
@ -4,20 +4,13 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Canape</title>
|
<title>Canapé</title>
|
||||||
|
|
||||||
<link href="/css/app.css" rel="stylesheet">
|
<link href="/css/app.css" rel="stylesheet">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{ template "errors" $ }}
|
<div id="app"></div>
|
||||||
{{ template "success" $ }}
|
|
||||||
{{ template "navbar" $ }}
|
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="container">
|
|
||||||
{{ yield }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
77
src/public/js/actions/actionCreators.js
Normal file
77
src/public/js/actions/actionCreators.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { configureAxios, request } from '../requests'
|
||||||
|
|
||||||
|
// Select Movie
|
||||||
|
export function selectMovie(index) {
|
||||||
|
return {
|
||||||
|
type: 'SELECT_MOVIE',
|
||||||
|
index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUserLoggedIn() {
|
||||||
|
return {
|
||||||
|
type: 'IS_USER_LOGGED_IN',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addError(message) {
|
||||||
|
return {
|
||||||
|
type: 'ADD_ERROR',
|
||||||
|
payload: {
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dismissError() {
|
||||||
|
return {
|
||||||
|
type: 'DISMISS_ERROR',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userLogout() {
|
||||||
|
return {
|
||||||
|
type: 'USER_LOGOUT',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loginUser(username, password) {
|
||||||
|
return request(
|
||||||
|
'USER_LOGIN',
|
||||||
|
configureAxios().post(
|
||||||
|
'/users/login',
|
||||||
|
{
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateUser(config) {
|
||||||
|
return request(
|
||||||
|
'USER_UPDATE',
|
||||||
|
configureAxios().post('/users/edit', config)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userSignUp(config) {
|
||||||
|
return request(
|
||||||
|
'USER_SIGNUP',
|
||||||
|
configureAxios().post('/users/signup', config)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserInfos() {
|
||||||
|
return request(
|
||||||
|
'GET_USER',
|
||||||
|
configureAxios().get('/users/details')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchMovies(url) {
|
||||||
|
return request(
|
||||||
|
'MOVIE_LIST_FETCH',
|
||||||
|
configureAxios().get(url)
|
||||||
|
)
|
||||||
|
}
|
@ -1,35 +1,90 @@
|
|||||||
var $ = require('jquery');
|
import 'babel-polyfill'
|
||||||
global.jQuery = global.$ = $;
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import { bindActionCreators } from 'redux'
|
||||||
|
import { Provider, connect } from 'react-redux'
|
||||||
|
import { Router, Route, IndexRoute, IndexRedirect, Link, hashHistory } from 'react-router'
|
||||||
|
import { routerActions } from 'react-router-redux'
|
||||||
|
import { UserAuthWrapper } from 'redux-auth-wrapper'
|
||||||
|
|
||||||
var bootstrap = require('bootstrap');
|
// Root reducer
|
||||||
|
import rootReducer from './reducers/index'
|
||||||
|
|
||||||
if($('#movie-library').length >0 ){
|
// Action creators
|
||||||
listSelector();
|
import * as actionCreators from './actions/actionCreators'
|
||||||
|
|
||||||
|
// Store
|
||||||
|
import store, { history } from './store'
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import NavBar from './components/navbar'
|
||||||
|
import Error from './components/errors'
|
||||||
|
import MovieList from './components/movies/list'
|
||||||
|
import UserLoginForm from './components/users/login'
|
||||||
|
import UserEdit from './components/users/edit'
|
||||||
|
import UserSignUp from './components/users/signup'
|
||||||
|
|
||||||
|
class Main extends React.Component {
|
||||||
|
componentWillMount() {
|
||||||
|
this.props.isUserLoggedIn()
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<NavBar {...this.props}/>
|
||||||
|
<Error {...this.props}/>
|
||||||
|
<div className="container-fluid">
|
||||||
|
<div className="container">
|
||||||
|
{React.cloneElement(this.props.children, this.props)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Help select elements form the list views
|
function mapStateToProps(state) {
|
||||||
function listSelector() {
|
return {
|
||||||
$first = $(".thumbnail").first();
|
movieStore: state.movieStore,
|
||||||
$first.addClass('thumbnail-selected');
|
userStore: state.userStore,
|
||||||
|
errors: state.errors,
|
||||||
// Get the detail
|
}
|
||||||
var $detail = $("#"+$first.data("imdbid")+"-detail" );
|
|
||||||
// Show it
|
|
||||||
$detail.removeClass("hidden");
|
|
||||||
$detail.addClass("show");
|
|
||||||
|
|
||||||
$(".thumbnail").click(function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
// Hide previous details
|
|
||||||
$(".movie-detail.show" ).addClass("hidden").removeClass("show");
|
|
||||||
|
|
||||||
// Change border on selected item
|
|
||||||
$('.thumbnail-selected').removeClass('thumbnail-selected');
|
|
||||||
$(this).addClass('thumbnail-selected');
|
|
||||||
|
|
||||||
// Get the detail
|
|
||||||
var $detail = $("#"+$(this).data("imdbid")+"-detail" );
|
|
||||||
// Show it
|
|
||||||
$detail.removeClass("hidden").addClass("show");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators(actionCreators, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const App = connect(mapStateToProps, mapDispatchToProps)(Main);
|
||||||
|
|
||||||
|
// Redirects to /login by default
|
||||||
|
const UserIsAuthenticated = UserAuthWrapper({
|
||||||
|
authSelector: state => state.userStore,
|
||||||
|
redirectAction: routerActions.replace,
|
||||||
|
wrapperDisplayName: 'UserIsAuthenticated',
|
||||||
|
predicate: user => user.isLogged,
|
||||||
|
failureRedirectPath: '/users/login',
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO find a better way
|
||||||
|
const MovieListPopular = (props) => (
|
||||||
|
<MovieList {...props} moviesUrl='/movies/explore/popular'/>
|
||||||
|
)
|
||||||
|
const MovieListPolochon = (props) => (
|
||||||
|
<MovieList {...props} moviesUrl='/movies/polochon'/>
|
||||||
|
)
|
||||||
|
|
||||||
|
ReactDOM.render((
|
||||||
|
<Provider store={store}>
|
||||||
|
<Router history={history}>
|
||||||
|
<Route path="/" component={App}>
|
||||||
|
<IndexRedirect to="/movies/polochon" />
|
||||||
|
<Route path="/users/login" component={UserLoginForm} />
|
||||||
|
<Route path="/users/signup" component={UserSignUp} />
|
||||||
|
<Route path="/users/edit" component={UserIsAuthenticated(UserEdit)} />
|
||||||
|
<Route path="/movies/popular" component={UserIsAuthenticated(MovieListPopular)} />
|
||||||
|
<Route path="/movies/polochon(/:page)" component={UserIsAuthenticated(MovieListPolochon)} />
|
||||||
|
</Route>
|
||||||
|
</Router>
|
||||||
|
</Provider>
|
||||||
|
),document.getElementById('app'));
|
||||||
|
19
src/public/js/components/errors.js
Normal file
19
src/public/js/components/errors.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function Error(props) {
|
||||||
|
if (!props.errors.message) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-6 col-md-offset-3 col-xs-12">
|
||||||
|
<div className="alert alert-warning">
|
||||||
|
<button type="button" className="close" onClick={props.dismissError}>
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
<p>{props.errors.message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
104
src/public/js/components/movies/list.js
Normal file
104
src/public/js/components/movies/list.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
function MoviePosters(props) {
|
||||||
|
// TODO handle the limit from the url
|
||||||
|
const perPage = 30;
|
||||||
|
|
||||||
|
let movies;
|
||||||
|
// Let's limit the number for now
|
||||||
|
if (props.movies.length > perPage) {
|
||||||
|
movies = props.movies.slice(0, perPage);
|
||||||
|
} else {
|
||||||
|
movies = props.movies;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="col-xs-5 col-md-8">
|
||||||
|
<div className="row">
|
||||||
|
{movies.map(function(movie, index) {
|
||||||
|
const selected = (index === props.selectedMovieIndex) ? true : false;
|
||||||
|
return (
|
||||||
|
<MoviePoster
|
||||||
|
data={movie}
|
||||||
|
key={movie.ID}
|
||||||
|
selected={selected}
|
||||||
|
onClick={() => props.onClick(index)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MoviePoster extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
src: `/img/movies/${this.props.data.imdb_id}.jpg`,
|
||||||
|
}
|
||||||
|
this.handleError = this.handleError.bind(this);
|
||||||
|
}
|
||||||
|
handleError() {
|
||||||
|
this.setState({ src: '/img/noimage.png' });
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const selected = this.props.selected ? ' thumbnail-selected' : '';
|
||||||
|
const imgClass = 'thumbnail' + selected;
|
||||||
|
return (
|
||||||
|
<div className="col-xs-12 col-md-3">
|
||||||
|
<a className={imgClass}>
|
||||||
|
<img
|
||||||
|
src={this.state.src}
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
onError={this.handleError}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieDetails(props) {
|
||||||
|
return (
|
||||||
|
<div className="col-xs-7 col-md-4">
|
||||||
|
<div className="movie-detail affix">
|
||||||
|
<h1 className="hidden-xs">{props.data.title}</h1>
|
||||||
|
<h3 className="visible-xs">{props.data.title}</h3>
|
||||||
|
<p>
|
||||||
|
<i className="fa fa-clock-o"></i>
|
||||||
|
{props.data.runtime} min
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i className="fa fa-star-o"></i>
|
||||||
|
{props.data.rating} <small>({props.data.votes} counts)</small>
|
||||||
|
</p>
|
||||||
|
<p className="movie-plot">{props.data.plot}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class MovieList extends React.Component {
|
||||||
|
componentWillMount() {
|
||||||
|
this.props.fetchMovies(this.props.moviesUrl);
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const movies = this.props.movieStore.movies;
|
||||||
|
const index = this.props.movieStore.selectedMovieIndex;
|
||||||
|
const selectedMovie = movies[index];
|
||||||
|
return (
|
||||||
|
<div className="row" id="movie-library">
|
||||||
|
<MoviePosters
|
||||||
|
movies={this.props.movieStore.movies}
|
||||||
|
selectedMovieIndex={index}
|
||||||
|
onClick={this.props.selectMovie}
|
||||||
|
/>
|
||||||
|
{selectedMovie &&
|
||||||
|
<MovieDetails data={selectedMovie} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
59
src/public/js/components/navbar.js
Normal file
59
src/public/js/components/navbar.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Link } from 'react-router'
|
||||||
|
import { store } from '../store'
|
||||||
|
import { isUserLoggedIn } from '../actions/actionCreators'
|
||||||
|
|
||||||
|
import { Nav, Navbar, NavItem, NavDropdown, MenuItem } from 'react-bootstrap'
|
||||||
|
import { LinkContainer } from 'react-router-bootstrap'
|
||||||
|
|
||||||
|
export default class NavBar extends React.Component {
|
||||||
|
render() {
|
||||||
|
const username = this.props.userStore.username;
|
||||||
|
const isLoggedIn = username !== "" ? true : false;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Navbar inverse fixedTop collapseOnSelect>
|
||||||
|
<Navbar.Header>
|
||||||
|
<LinkContainer to="/">
|
||||||
|
<Navbar.Brand><a href="#">Canapé</a></Navbar.Brand>
|
||||||
|
</LinkContainer>
|
||||||
|
<Navbar.Toggle />
|
||||||
|
</Navbar.Header>
|
||||||
|
<Navbar.Collapse>
|
||||||
|
<Nav>
|
||||||
|
<LinkContainer to="/movies/popular">
|
||||||
|
<NavItem>Popular movies</NavItem>
|
||||||
|
</LinkContainer>
|
||||||
|
<LinkContainer to="/movies/polochon">
|
||||||
|
<NavItem>Polochon movies</NavItem>
|
||||||
|
</LinkContainer>
|
||||||
|
</Nav>
|
||||||
|
<Nav pullRight>
|
||||||
|
{isLoggedIn ||
|
||||||
|
<LinkContainer to="/users/signup">
|
||||||
|
<NavItem>Sign up</NavItem>
|
||||||
|
</LinkContainer>
|
||||||
|
}
|
||||||
|
{isLoggedIn ||
|
||||||
|
<LinkContainer to="/users/login">
|
||||||
|
<NavItem>Login</NavItem>
|
||||||
|
</LinkContainer>
|
||||||
|
}
|
||||||
|
{isLoggedIn &&
|
||||||
|
<NavDropdown title={username} id="navbar-dropdown-right">
|
||||||
|
<LinkContainer to="/users/edit">
|
||||||
|
<MenuItem>Edit</MenuItem>
|
||||||
|
</LinkContainer>
|
||||||
|
<LinkContainer to="/users/logout" onClick={this.props.userLogout}>
|
||||||
|
<MenuItem>Logout</MenuItem>
|
||||||
|
</LinkContainer>
|
||||||
|
</NavDropdown>
|
||||||
|
}
|
||||||
|
</Nav>
|
||||||
|
</Navbar.Collapse>
|
||||||
|
</Navbar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
63
src/public/js/components/users/edit.js
Normal file
63
src/public/js/components/users/edit.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default class UserEdit extends React.Component {
|
||||||
|
componentWillMount() {
|
||||||
|
this.props.getUserInfos();
|
||||||
|
}
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.updateUser({
|
||||||
|
'polochon_url': this.refs.polochonUrl.value,
|
||||||
|
'polochon_token': this.refs.polochonToken.value,
|
||||||
|
'password': this.refs.newPassword.value,
|
||||||
|
'password_confirm': this.refs.newPasswordConfirm.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
// TODO make this fields editable
|
||||||
|
const polochonUrl = this.props.userStore.polochonUrl;
|
||||||
|
const polochonToken = this.props.userStore.polochonToken;
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="content-fluid">
|
||||||
|
<div className="col-md-6 col-md-offset-3 col-xs-12">
|
||||||
|
<h2>Edit user</h2>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<form ref="userEditForm" className="form-horizontal" onSubmit={(e) => this.handleSubmit(e)}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="control-label">Polochon URL</label>
|
||||||
|
<input autoFocus="autofocus" className="form-control" type="text" ref="polochonUrl" value={polochonUrl} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="control-label">Polochon token</label>
|
||||||
|
<input className="form-control" ref="polochonToken" type="text" value={polochonToken} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="control-label">Password</label>
|
||||||
|
<input autoComplete="off" className="form-control" ref="newPassword" type="password"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="control-label">Confirm Password</label>
|
||||||
|
<input autoComplete="off" className="form-control" ref="newPasswordConfirm" type="password"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input className="btn btn-primary pull-right" type="submit" value="Update"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
66
src/public/js/components/users/login.js
Normal file
66
src/public/js/components/users/login.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default class UserLoginForm extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (!nextProps.userStore.isLogged) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!nextProps.location.query.redirect) {
|
||||||
|
// Redirect home
|
||||||
|
nextProps.router.push('/');
|
||||||
|
} else {
|
||||||
|
// Redirect to the previous page
|
||||||
|
nextProps.router.push(nextProps.location.query.redirect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.props.userStore.userLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const username = this.refs.username.value;
|
||||||
|
const password = this.refs.password.value;
|
||||||
|
this.props.loginUser(username, password);
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="content-fluid">
|
||||||
|
<div className="col-md-6 col-md-offset-3 col-xs-12">
|
||||||
|
<h2>Log in</h2>
|
||||||
|
<hr/>
|
||||||
|
<form ref="loginForm" className="form-horizontal" onSubmit={(e) => this.handleSubmit(e)}>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="user_email">Username</label>
|
||||||
|
<br/>
|
||||||
|
<input className="form-control" type="username" ref="username"/>
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="user_password">Password</label>
|
||||||
|
<br/>
|
||||||
|
<input className="form-control" type="password" ref="password"/>
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{this.props.userStore.userLoading &&
|
||||||
|
<button className="btn btn-primary pull-right">
|
||||||
|
<i className="fa fa-spinner fa-spin"></i>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
{this.props.userStore.userLoading ||
|
||||||
|
<input className="btn btn-primary pull-right" type="submit" value="Log in"/>
|
||||||
|
}
|
||||||
|
<br/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
48
src/public/js/components/users/signup.js
Normal file
48
src/public/js/components/users/signup.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default class UserSignUp extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.userSignUp({
|
||||||
|
'username': this.refs.username.value,
|
||||||
|
'password': this.refs.password.value,
|
||||||
|
'password_confirm': this.refs.passwordConfirm.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="content-fluid">
|
||||||
|
<div className="col-md-6 col-md-offset-3 col-xs-12">
|
||||||
|
<h2>Sign up</h2>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<form ref="userSignUpForm" className="form-horizontal" onSubmit={(e) => this.handleSubmit(e)}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="control-label">Username</label>
|
||||||
|
<input autoFocus="autofocus" className="form-control" type="text" ref="username" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="control-label">Password</label>
|
||||||
|
<input className="form-control" ref="password" type="password" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="control-label">Password confirm</label>
|
||||||
|
<input className="form-control" ref="passwordConfirm" type="password" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input className="btn btn-primary pull-right" type="submit" value="Sign up"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
12
src/public/js/reducers/errors.js
Normal file
12
src/public/js/reducers/errors.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export default function error(state = {}, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'ADD_ERROR':
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
message: action.payload.message,
|
||||||
|
})
|
||||||
|
case 'DISMISS_ERROR':
|
||||||
|
return {};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
15
src/public/js/reducers/index.js
Normal file
15
src/public/js/reducers/index.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import { routerReducer } from 'react-router-redux'
|
||||||
|
|
||||||
|
import movieStore from './movies'
|
||||||
|
import userStore from './users'
|
||||||
|
import errors from './errors'
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
routing: routerReducer,
|
||||||
|
movieStore,
|
||||||
|
userStore,
|
||||||
|
errors,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default rootReducer;
|
21
src/public/js/reducers/movies.js
Normal file
21
src/public/js/reducers/movies.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
const defaultState = {
|
||||||
|
movies: [],
|
||||||
|
selectedMovieIndex: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function movieStore(state = defaultState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'MOVIE_LIST_FETCH_FULFILLED':
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
movies: action.payload.data,
|
||||||
|
})
|
||||||
|
case 'MOVIE_LIST_FETCH_PENDING':
|
||||||
|
return state
|
||||||
|
case 'SELECT_MOVIE':
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
selectedMovieIndex: action.index,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
55
src/public/js/reducers/users.js
Normal file
55
src/public/js/reducers/users.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import jwtDecode from 'jwt-decode'
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
userLoading: false,
|
||||||
|
username: "",
|
||||||
|
isAdmin: false,
|
||||||
|
isLogged: false,
|
||||||
|
polochonToken: "",
|
||||||
|
polochonUrl: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function userStore(state = defaultState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'USER_LOGIN_PENDING':
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
userLoading: true,
|
||||||
|
})
|
||||||
|
case 'USER_LOGIN_FULFILLED':
|
||||||
|
if (action.payload.status === "error") {
|
||||||
|
return logoutUser(state)
|
||||||
|
}
|
||||||
|
return updateFromToken(state, action.payload.data.token)
|
||||||
|
case 'IS_USER_LOGGED_IN':
|
||||||
|
let localToken = localStorage.getItem('token');
|
||||||
|
if (!localToken || localToken === "") {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return updateFromToken(state, localToken)
|
||||||
|
case 'USER_LOGOUT':
|
||||||
|
return logoutUser(state)
|
||||||
|
case 'GET_USER_FULFILLED':
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
polochonToken: action.payload.data.token,
|
||||||
|
polochonUrl: action.payload.data.url,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logoutUser(state) {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
return Object.assign({}, state, defaultState)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFromToken(state, token) {
|
||||||
|
const decodedToken = jwtDecode(token);
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
userLoading: false,
|
||||||
|
isLogged: true,
|
||||||
|
isAdmin: decodedToken.isAdmin,
|
||||||
|
username: decodedToken.username,
|
||||||
|
})
|
||||||
|
}
|
59
src/public/js/requests.js
Normal file
59
src/public/js/requests.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
// This functions returns an axios instance, the token is added to the
|
||||||
|
// configuration if found in the localStorage
|
||||||
|
export function configureAxios(headers = {}) {
|
||||||
|
// Get the token from the localStorate
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
headers = { 'Authorization': `Bearer ${token}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios.create({
|
||||||
|
headers
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function takes en event prefix to dispatch evens during the life of the
|
||||||
|
// request, it also take a promise (axios request)
|
||||||
|
export function request(eventPrefix, promise) {
|
||||||
|
// Events
|
||||||
|
const pending = `${eventPrefix}_PENDING`;
|
||||||
|
const fulfilled = `${eventPrefix}_FULFILLED`;
|
||||||
|
|
||||||
|
return function(dispatch) {
|
||||||
|
dispatch({
|
||||||
|
type: pending,
|
||||||
|
})
|
||||||
|
promise
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.status === 'error')
|
||||||
|
{
|
||||||
|
dispatch({
|
||||||
|
type: 'ADD_ERROR',
|
||||||
|
payload: {
|
||||||
|
message: response.data.message,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
dispatch({
|
||||||
|
type: fulfilled,
|
||||||
|
payload: response.data,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Unauthorized
|
||||||
|
if (error.response.status == 401) {
|
||||||
|
dispatch({
|
||||||
|
type: 'USER_LOGOUT',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
dispatch({
|
||||||
|
type: 'ADD_ERROR',
|
||||||
|
payload: {
|
||||||
|
message: error.response.data,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
27
src/public/js/store.js
Normal file
27
src/public/js/store.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
|
import { syncHistoryWithStore } from 'react-router-redux'
|
||||||
|
import { hashHistory } from 'react-router'
|
||||||
|
import thunk from 'redux-thunk'
|
||||||
|
import { routerMiddleware } from 'react-router-redux'
|
||||||
|
|
||||||
|
// Import the root reducer
|
||||||
|
import rootReducer from './reducers/index'
|
||||||
|
|
||||||
|
const routingMiddleware = routerMiddleware(hashHistory)
|
||||||
|
|
||||||
|
const middlewares = [thunk, routingMiddleware];
|
||||||
|
|
||||||
|
// Only use in development mode (set in webpack)
|
||||||
|
if (process.env.NODE_ENV === `development`) {
|
||||||
|
const createLogger = require(`redux-logger`);
|
||||||
|
const logger = createLogger();
|
||||||
|
middlewares.push(logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the store
|
||||||
|
const store = compose(applyMiddleware(...middlewares))(createStore)(rootReducer);
|
||||||
|
|
||||||
|
// Sync history with store
|
||||||
|
export const history = syncHistoryWithStore(hashHistory, store);
|
||||||
|
|
||||||
|
export default store;
|
@ -1,9 +0,0 @@
|
|||||||
{{ if $.Data.Errors }}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 col-md-offset-3 col-xs-12">
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<p>{{ $.Data.Errors }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
@ -1,61 +0,0 @@
|
|||||||
{{ if $.Data.error }}
|
|
||||||
|
|
||||||
{{ if eq $.Data.error "Invalid address"}}
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
The polochon API adress specified in your configuration is invalid or unreachable,
|
|
||||||
<a href="{{ URL "users.edit"}}">change it</a>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ else }}
|
|
||||||
|
|
||||||
<div class="row" id="movie-library">
|
|
||||||
|
|
||||||
<div class="col-xs-5 col-md-8">
|
|
||||||
<div class="row">
|
|
||||||
{{ range $.Data.movies }}
|
|
||||||
<div class="col-xs-12 col-md-3">
|
|
||||||
<a href="#" class="thumbnail" data-imdbid="{{.ImdbID}}">
|
|
||||||
<img src="/img/movies/{{.ImdbID}}.jpg" onerror="this.onerror=null;this.src='/img/noimage.png';">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-xs-7 col-md-4">
|
|
||||||
{{ range $.Data.movies}}
|
|
||||||
<div id="{{.ImdbID}}-detail" class="hidden movie-detail affix">
|
|
||||||
<h1 class="hidden-xs">{{ .Title }}</h1>
|
|
||||||
<h3 class="visible-xs">{{ .Title }}</h3>
|
|
||||||
<h4 class="hidden-xs">{{ .Year }}</h4>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<i class="fa fa-clock-o"></i>
|
|
||||||
{{ .Runtime }} min
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<i class="fa fa-star-o"></i>
|
|
||||||
{{ .Rating }} <small>({{ .Votes }} counts)</small>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="movie-plot">{{ .Plot }}</p>
|
|
||||||
|
|
||||||
<div class="movie-details-buttons">
|
|
||||||
{{ if .PolochonURL }}
|
|
||||||
<a type="button" class="btn btn-success btn-sm" href="{{ .PolochonURL }}">
|
|
||||||
<i class="fa fa-download"></i> Download
|
|
||||||
</a>
|
|
||||||
{{ end }}
|
|
||||||
<a id="imdb-link" type="button" class="btn btn-sm btn-warning" href="http://www.imdb.com/title/{{ .ImdbID }}">
|
|
||||||
<i class="fa fa-external-link"></i> IMDB
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ end }}
|
|
@ -1,71 +0,0 @@
|
|||||||
<nav class="navbar navbar-inverse navbar-fixed-top">
|
|
||||||
<div class="container">
|
|
||||||
<div class="container-fluid">
|
|
||||||
<!-- Brand and toggle get grouped for better mobile display -->
|
|
||||||
<div class="navbar-header">
|
|
||||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
|
|
||||||
<span class="sr-only">Toggle navigation</span>
|
|
||||||
<span class="icon-bar"></span>
|
|
||||||
<span class="icon-bar"></span>
|
|
||||||
<span class="icon-bar"></span>
|
|
||||||
</button>
|
|
||||||
<a class="navbar-brand" href="#">Canape</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Collect the nav links, forms, and other content for toggling -->
|
|
||||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
|
|
||||||
<ul class="nav navbar-nav">
|
|
||||||
<li><a href="{{ URL "movies.explore.popular"}}">Movies</a></li>
|
|
||||||
<li><a href="#">TV Shows</a></li>
|
|
||||||
{{ if $.Data.user }}
|
|
||||||
<li class="dropdown">
|
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
|
||||||
<i class="fa fa-list"></i>
|
|
||||||
My Library
|
|
||||||
<span class="caret"></span>
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu" role="menu">
|
|
||||||
<li {{ if eq $.Route "movies.polochon" }}class="active"{{ end }}>
|
|
||||||
<a href="{{ URL "movies.polochon" }}?limit=10">Movies{{ if eq $.Route "movies.polochon" }}<span class="sr-only">Movies</span>{{ end }}</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#">
|
|
||||||
TVShows
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{{ end }}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<ul class="nav navbar-nav navbar-right">
|
|
||||||
{{ if $.Data.user }}
|
|
||||||
<li class="dropdown">
|
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{{$.Data.user.Name}} <span class="caret"></span></a>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li>
|
|
||||||
<a href="{{ URL "users.details"}}">My account</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{{ URL "users.edit"}}">Edit</a>
|
|
||||||
</li>
|
|
||||||
<li role="separator" class="divider"></li>
|
|
||||||
<li>
|
|
||||||
<a href="{{ URL "users.logout"}}">Logout</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{{ else }}
|
|
||||||
<li>
|
|
||||||
<a href="{{ URL "users.login"}}">Sign in</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{{ URL "users.signup"}}">Sign up</a>
|
|
||||||
</li>
|
|
||||||
{{ end }}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div><!-- /.navbar-collapse -->
|
|
||||||
</div><!-- /.container-fluid -->
|
|
||||||
</div><!-- /.container -->
|
|
||||||
</nav>
|
|
@ -1,9 +0,0 @@
|
|||||||
{{ if $.Data.Success }}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12">
|
|
||||||
<div class="alert alert-success">
|
|
||||||
{{ $.Data.Success }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
@ -1,26 +0,0 @@
|
|||||||
<div class="container">
|
|
||||||
<div id="user-details" class="content-fluid">
|
|
||||||
|
|
||||||
<div class="col-md-6 col-md-offset-3 col-xs-12">
|
|
||||||
<h2>{{ $.Data.user.Name }}'s informations</h2>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div class="panel panel-info">
|
|
||||||
<div class="panel-heading">
|
|
||||||
Polochon URL
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">{{ $.Data.polochon.URL }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="panel panel-info">
|
|
||||||
<div class="panel-heading">
|
|
||||||
Polochon token
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">{{ $.Data.polochon.Token }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<a class="btn btn-default pull-left" href="{{ URL "users.edit" }}">Edit</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,40 +0,0 @@
|
|||||||
<div class="container">
|
|
||||||
<div class="content-fluid">
|
|
||||||
<div class="col-md-6 col-md-offset-3 col-xs-12">
|
|
||||||
<h2>Edit user</h2>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<form accept-charset="UTF-8" action="{{ URL "users.edit" }}" method="POST" class="form-horizontal" id="user">
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="control-label" for="PolochonURL">Polochon URL</label>
|
|
||||||
<input autofocus="autofocus" class="form-control" name="PolochonURL" type="text" value="{{ $.Data.polochon.URL }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="control-label" for="PolochonToken">Polochon token</label>
|
|
||||||
<input autofocus="autofocus" class="form-control" name="PolochonToken" type="text" value="{{ $.Data.polochon.Token }}" >
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="control-label" for="Password">Password</label>
|
|
||||||
<input autocomplete="off" class="form-control" name="Password" type="password" value="">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="control-label" for="PasswordVerify">Confirm Password</label>
|
|
||||||
<input autocomplete="off" class="form-control" name="PasswordVerify" type="password" value="">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<input class="btn btn-primary pull-right" type="submit" value="Update">
|
|
||||||
<a class="btn btn-default pull-left" href="{{ URL "users.details" }}">Cancel</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
|||||||
<div class="container">
|
|
||||||
<div class="content-fluid">
|
|
||||||
<div class="col-md-6 col-md-offset-3 col-xs-12">
|
|
||||||
<h2>Log in</h2>
|
|
||||||
<hr>
|
|
||||||
<form accept-charset="UTF-8" action="/users/login" method="POST" class="form-horizontal">
|
|
||||||
<div>
|
|
||||||
<label for="user_email">Username</label>
|
|
||||||
<br>
|
|
||||||
<input autofocus="autofocus" class="form-control" id="username" name="Username" type="username" value="">
|
|
||||||
<p></p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="user_password">Password</label>
|
|
||||||
<br>
|
|
||||||
<input autocomplete="off" class="form-control" id="password" name="Password" type="password">
|
|
||||||
<p></p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input class="btn btn-primary pull-right" type="submit" value="Log in">
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
<a class="btn btn-default btn-sm pull-left" href="/movies/%3cnil%3e">Cancel</a>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,39 +0,0 @@
|
|||||||
<div class="container">
|
|
||||||
<div class="content-fluid">
|
|
||||||
<div class="col-md-6 col-md-offset-3 col-xs-12">
|
|
||||||
<h2>Sign up</h2>
|
|
||||||
<hr>
|
|
||||||
<form accept-charset="UTF-8" action="{{ URL "users.signup" }}" method="POST" class="form-horizontal" id="new_user">
|
|
||||||
<div>
|
|
||||||
<p class="">
|
|
||||||
<label for="user_email">Username</label>
|
|
||||||
<br>
|
|
||||||
<input autofocus="autofocus" class="form-control" id="username" name="Username" type="username" value="">
|
|
||||||
<span class="error"></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="">
|
|
||||||
<label for="user_password">Password</label>
|
|
||||||
<br>
|
|
||||||
<input autocomplete="off" class="form-control" id="password" name="Password" type="password" value="">
|
|
||||||
<span class="error"></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="">
|
|
||||||
<label for="user_password">Confirm Password</label>
|
|
||||||
<br>
|
|
||||||
<input autocomplete="off" class="form-control" id="passwordVerify" name="PasswordVerify" type="password" value="">
|
|
||||||
<span class="error"></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input class="btn btn-primary pull-right" type="submit" value="Sign up">
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
<a class="btn btn-default btn-sm pull-left" href="/movies/%3cnil%3e">Cancel</a>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
29
webpack.config.babel.js
Normal file
29
webpack.config.babel.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
var webpack = require("webpack");
|
||||||
|
|
||||||
|
export default {
|
||||||
|
output: {
|
||||||
|
filename: 'app.js',
|
||||||
|
},
|
||||||
|
devtool: 'source-map',
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
test: /\.jsx?$/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
exclude: [/node_modules/],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
$: "jquery",
|
||||||
|
jQuery: "jquery"
|
||||||
|
}),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env.NODE_ENV': JSON.stringify('development')
|
||||||
|
})
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
extensions: ['', '.js', '.jsx'],
|
||||||
|
},
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user