Compare commits

..

16 Commits

Author SHA1 Message Date
ab90365c9c Merge branch 'develop' into dist/ubuntu/bionic/develop 2020-07-17 09:40:03 +02:00
303ea6b1d6 Stockage des sessions en base de données via GORM 2020-07-17 09:39:37 +02:00
ccf911322b Correction make up 2020-07-17 09:39:02 +02:00
0cb6c7c67e Merge branch 'feature/user-profile' of Cadoles/daddy into develop 2020-07-17 09:25:50 +02:00
ec6de8a217 Création du lien symbolique vers la configuration du client 2020-07-17 09:24:03 +02:00
17cd58d68f Merge branch 'develop' into dist/ubuntu/bionic/develop 2020-07-16 22:51:53 +02:00
08bd11f4d9 Simplification Makefile 2020-07-16 22:51:26 +02:00
99fb4ac6d9 Correction recette packaging 2020-07-16 22:50:55 +02:00
2ceba1f219 Merge branch 'develop' into dist/ubuntu/bionic/develop 2020-07-16 22:35:27 +02:00
7122677351 Mise à jour règles packaging Debian 2020-07-16 22:33:51 +02:00
0d308acd5c Ajout script/commande de release 2020-07-16 22:31:02 +02:00
36c253d4d7 Correction nom projet client 2020-07-16 22:30:03 +02:00
ed219ddd11 Correction typo annotation 2020-07-16 22:29:33 +02:00
758c166f27 Simple page de modification de profil 2020-07-16 20:21:58 +02:00
05dd505d6b Bascule sur l'ORM GORM
- On n'utilise plus la pattern CQRS trop lourde pour le système
- Un système de models/repository "à la Symfony" est utilisé pour les
  requêtes
2020-07-16 14:30:16 +02:00
1d526a37d0 Empaquetage Debian basique 2020-06-17 18:58:27 +02:00
64 changed files with 1117 additions and 718 deletions

View File

@ -4,4 +4,4 @@ OIDC_CLIENT_SECRET=daddycool
OIDC_POST_LOGOUT_REDIRECT_URL=http://localhost:8081/logout/redirect
HTTP_COOKIE_AUTHENTICATION_KEY=cL87ucJJSGt7XSjRuQe7GDb2qna8ijfQ
HTTP_COOKIE_ENCRYPTION_KEY=cL87ucJJSGt7XSjRuQe7GDb2qna8ijfQ
DATABASE_DSN="host=localhost user=daddy database=daddy password=daddy"
DATABASE_DSN="host=localhost user=daddy database=daddy password=daddy port=5432 sslmode=disable"

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
/vendor
/data
/bin
/.env
/.env
/release

View File

@ -1,12 +1,12 @@
SHELL := /bin/bash
build: build-docker build-server
build: build-server
build-docker:
docker:
docker-compose build
generate:
cd internal && go run github.com/99designs/gqlgen generate
go generate ./...
build-server:
CGO_ENABLED=0 go build -v -o ./bin/server ./cmd/server
@ -15,7 +15,10 @@ deps: generate
cd client && npm install
go get ./...
up: build-docker
client-dist:
cd client && NODE_ENV=production npm run build
up: docker
docker-compose up
watch:
@ -45,7 +48,11 @@ test:
hydra-shell:
docker-compose exec hydra /bin/sh
clean: down
.PHONY: release
release:
./misc/script/release
clean:
rm -rf client/node_modules bin data .env internal/graph/generated internal/graph/server.go
rm -rf vendor
go clean -modcache

View File

@ -29,7 +29,7 @@ Les services suivants devraient être disponibles après démarrage de l'environ
|Service|Type|Accès|Description|
|-------|----|-----|-----------|
|Application React|HTTP (UI)|http://localhost:8080/|Page d'accueil de l'application React (serveur Webpack)|
|Interface Web GraphQL|HTTP (UI)|http://localhost:8081/api/v1/graphql (GET)|Interface Web de développement de l'API GraphQL (mode debug uniquement, nécessite d'être authentifié)|
|Interface Web GraphQL|HTTP (UI)|http://localhost:8081/api/v1/playground|Interface Web de développement de l'API GraphQL (mode debug uniquement, nécessite d'être authentifié)|
|Serveur GraphQL|HTTP (GraphQL)|http://localhost:8081/api/v1/graphql (POST)|Point d'entrée de l'API GraphQL|
|Serveur Hydra|HTTP (ReST)|http://localhost:4444|Point d'entrée pour l'API OAuth2 d'[Hydra](https://www.ory.sh/hydra/docs/)|
|Serveur Hydra Passwordless|HTTP|http://localhost:3000|Point d'entrée pour la ["Login/Consent App"](https://www.ory.sh/hydra/docs/implementing-consent) [hydra-passwordless](https://forge.cadoles.com/wpetit/hydra-passwordless)|

View File

@ -1,5 +1,5 @@
{
"name": "dadd-",
"name": "daddy",
"version": "0.0.0",
"description": "Daddy",
"main": "index.js",

View File

@ -3,6 +3,7 @@ import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
import { HomePage } from './HomePage/HomePage';
import { store } from '../store/store';
import { Provider } from 'react-redux';
import { ProfilePage } from './ProfilePage/ProfilePage';
export class App extends React.Component {
render() {
@ -11,6 +12,7 @@ export class App extends React.Component {
<BrowserRouter>
<Switch>
<Route path="/" exact component={HomePage} />
<Route path="/profile" exact component={ProfilePage} />
<Route component={() => <Redirect to="/" />} />
</Switch>
</BrowserRouter>

View File

@ -7,7 +7,7 @@ export function HomePage() {
const currentUser = useSelector((state: RootState) => state.auth.currentUser);
return (
<Page title="Daddy - Accueil">
<Page title="Accueil">
<div className="container is-fluid">
<section className="section">
<div className="columns">
@ -15,7 +15,7 @@ export function HomePage() {
<div className="box">
{
currentUser && currentUser.email ?
<p>Bonjour <span className="has-text-weight-bold">{currentUser.email}</span> !</p> :
<p>Bonjour <span className="has-text-weight-bold">{currentUser.name ? currentUser.name : currentUser.email}</span> !</p> :
<p>Veuillez vous authentifier.</p>
}
</div>

View File

@ -1,44 +1,62 @@
import React from 'react';
import React, { Fragment, useState } from 'react';
import logo from '../resources/logo.svg';
import { useSelector } from 'react-redux';
import { RootState } from '../store/reducers/root';
import { Config } from '../config';
import { Link } from 'react-router-dom';
export function Navbar() {
const isAuthenticated = useSelector<RootState>(state => state.auth.isAuthenticated);
const [ isActive, setActive ] = useState(false);
const toggleMenu = () => {
setActive(active => !active);
};
return (
<nav className="navbar" role="navigation" aria-label="main navigation">
<div className="container is-fluid">
<div className="navbar-brand">
<a className="navbar-item" href="#/">
<Link className="navbar-item" to="/">
<img src={logo} style={{marginRight:'5px',width:'28px',height:'28px'}} />
<h1 className="is-size-4">Daddy</h1>
</a>
<a role="button" className="navbar-burger" aria-label="menu" aria-expanded="false">
</Link>
<a role="button"
className={`navbar-burger ${isActive ? 'is-active' : ''}`}
onClick={toggleMenu}
aria-label="menu"
aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div className="navbar-menu">
<div className={`navbar-menu ${isActive ? 'is-active' : ''}`}>
<div className="navbar-end">
<div className="navbar-item">
<div className="buttons">
{
isAuthenticated ?
<a className="button is-small" href={Config.logoutURL}>
<span className="icon">
<i className="fas fa-sign-out-alt"></i>
</span>
<span>Se déconnecter</span>
</a> :
<a className="button is-small" href={Config.loginURL}>
<Fragment>
<Link to="/profile" className="button">
<span className="icon">
<i className="fas fa-user"></i>
</span>
</Link>
<a className="button" href={Config.logoutURL}>
<span className="icon">
<i className="fas fa-sign-out-alt"></i>
</span>
</a>
</Fragment> :
<a className="button" href={Config.loginURL}>
<span className="icon">
<i className="fas fa-sign-in-alt"></i>
</span>
<span>Se connecter</span>
</a>
}
</div>
</div>
</div>
</div>

View File

@ -25,6 +25,6 @@ export class Page extends React.PureComponent<PageProps> {
updateTitle() {
const { title } = this.props;
if (title !== undefined) window.document.title = title;
if (title !== undefined) window.document.title = title + ' - Daddy';
}
}

View File

@ -0,0 +1,38 @@
import React, { useEffect } from 'react';
import { Page } from '../Page';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../../store/reducers/root';
import { UserForm } from '../UserForm';
import { Loader } from '../Loader';
import { User } from '../../types/user';
import { updateProfile } from '../../store/actions/profile';
export function ProfilePage() {
const currentUser = useSelector((state: RootState) => state.auth.currentUser);
const dispatch = useDispatch();
const onUserChange = (user: User) => {
if (currentUser.name !== user.name) {
dispatch(updateProfile({ name: user.name }))
}
};
return (
<Page title="Mon profil">
<div className="container is-fluid">
<section className="section">
<div className="columns">
<div className="column is-6 is-offset-3">
<h2 className="is-size-2 subtitle">Mon profil</h2>
{
currentUser ?
<UserForm onChange={onUserChange} user={currentUser} /> :
<Loader />
}
</div>
</div>
</section>
</div>
</Page>
);
}

View File

@ -0,0 +1,80 @@
import React, { useState, ChangeEvent, useEffect } from 'react';
import { User } from '../types/user';
export interface UserFormProps {
user: User
onChange?: (user: User) => void
}
export function UserForm({ user, onChange }: UserFormProps) {
const [ state, setState ] = useState({
changed: false,
user: {
name: '',
email: '',
createdAt: null,
connectedAt: null,
...user,
}
});
const onSaveClick = () => {
if (!state.changed) return;
if (typeof onChange !== 'function') return;
onChange(state.user);
setState(state => {
return {
...state,
changed: false,
};
})
};
const onUserAttrChange = function(attrName: string, evt: ChangeEvent<HTMLInputElement>) {
const value = evt.currentTarget.value;
setState(state => {
return {
...state,
changed: true,
user: {
...state.user,
[attrName]: value,
}
};
});
};
return (
<div className="form">
<div className="field">
<label className="label">Nom d'utilisateur</label>
<div className="control">
<input type="text" className="input" value={state.user.name}
onChange={onUserAttrChange.bind(null, "name")} />
</div>
</div>
<div className="field">
<label className="label">Adresse courriel</label>
<div className="control">
<p className="input is-static">{state.user.email}</p>
</div>
</div>
<div className="field">
<label className="label">Date de dernière connexion</label>
<div className="control">
<p className="input is-static">{state.user.connectedAt}</p>
</div>
</div>
<div className="field">
<label className="label">Date de création</label>
<div className="control">
<p className="input is-static">{state.user.createdAt}</p>
</div>
</div>
<div className="buttons is-right">
<button disabled={!state.changed}
className="button is-primary" onClick={onSaveClick}>Enregistrer</button>
</div>
</div>
);
}

View File

@ -16,4 +16,25 @@ export interface fetchProfileSuccessAction extends Action {
export function fetchProfile(): fetchProfileRequestAction {
return { type: FETCH_PROFILE_REQUEST }
}
export const UPDATE_PROFILE_REQUEST = 'UPDATE_PROFILE_REQUEST';
export const UPDATE_PROFILE_SUCCESS = 'UPDATE_PROFILE_SUCCESS';
export const UPDATE_PROFILE_FAILURE = 'UPDATE_PROFILE_FAILURE';
export interface ProfileChanges {
name?: string
}
export interface updateProfileRequestAction extends Action {
changes: ProfileChanges
}
export interface updateProfileSuccessAction extends Action {
profile: User
}
export function updateProfile(changes: ProfileChanges): updateProfileRequestAction {
return { type: UPDATE_PROFILE_REQUEST, changes }
}

View File

@ -1,7 +1,7 @@
import { Action } from "redux";
import { User } from "../../types/user";
import { SET_CURRENT_USER, setCurrentUserAction } from "../actions/auth";
import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction } from "../actions/profile";
import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction, updateProfileSuccessAction, UPDATE_PROFILE_SUCCESS, updateProfileRequestAction } from "../actions/profile";
export interface AuthState {
isAuthenticated: boolean
@ -19,6 +19,8 @@ export function authReducer(state = defaultState, action: Action): AuthState {
return handleSetCurrentUser(state, action as setCurrentUserAction);
case FETCH_PROFILE_SUCCESS:
return handleFetchProfileSuccess(state, action as fetchProfileSuccessAction);
case UPDATE_PROFILE_SUCCESS:
return handleFetchProfileSuccess(state, action as updateProfileSuccessAction);
}
return state;
@ -39,9 +41,17 @@ function handleFetchProfileSuccess(state: AuthState, { profile }: fetchProfileSu
...state,
isAuthenticated: true,
currentUser: {
email: profile.email,
connectedAt: profile.connectedAt,
createdAt: profile.createdAt,
...profile,
}
};
};
function handleUpdateProfileSuccess(state: AuthState, { profile }: updateProfileSuccessAction): AuthState {
return {
...state,
isAuthenticated: true,
currentUser: {
...profile,
}
};
};

View File

@ -1,15 +1,15 @@
import { DaddyClient, getClient } from "../../util/daddy";
import { Config } from "../../config";
import { all, takeLatest, put, select } from "redux-saga/effects";
import { FETCH_PROFILE_REQUEST, fetchProfile, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS } from "../actions/profile";
import { FETCH_PROFILE_REQUEST, fetchProfile, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS, updateProfileRequestAction, UPDATE_PROFILE_REQUEST, UPDATE_PROFILE_FAILURE, UPDATE_PROFILE_SUCCESS } from "../actions/profile";
import { SET_CURRENT_USER } from "../actions/auth";
import { RootState } from "../reducers/root";
import { User } from "../../types/user";
export function* usersRootSaga() {
export function* profileRootSaga() {
yield all([
takeLatest(SET_CURRENT_USER, onCurrentUserChangeSaga),
takeLatest(FETCH_PROFILE_REQUEST, fetchProfileSaga),
takeLatest(UPDATE_PROFILE_REQUEST, updateProfileSaga),
]);
}
@ -23,11 +23,24 @@ export function* fetchProfileSaga() {
let profile: User;
try {
profile = yield client.fetchProfile().then(result => result.userProfile);
console.log(profile);
} catch(err) {
yield put({ type: FETCH_PROFILE_FAILURE, err });
return;
}
yield put({type: FETCH_PROFILE_SUCCESS, profile });
}
export function* updateProfileSaga({ changes }: updateProfileRequestAction) {
const client = getClient(Config.graphQLEndpoint, Config.subscriptionEndpoint);
let profile: User;
try {
profile = yield client.updateProfile(changes).then(result => result.updateProfile);
} catch(err) {
yield put({ type: UPDATE_PROFILE_FAILURE, err });
return;
}
yield put({type: UPDATE_PROFILE_SUCCESS, profile });
}

View File

@ -1,12 +1,12 @@
import { all } from 'redux-saga/effects';
import { failureRootSaga } from './failure';
import { initRootSaga } from './init';
import { usersRootSaga } from './users';
import { profileRootSaga } from './profile';
export function* rootSaga() {
yield all([
initRootSaga(),
failureRootSaga(),
usersRootSaga(),
profileRootSaga(),
]);
}

View File

@ -1,5 +1,6 @@
export interface User {
email: string
name?: string
connectedAt?: Date
createdAt?: Date
}

View File

@ -1,10 +1,11 @@
import ApolloClient from 'apollo-client';
import ApolloClient, { DefaultOptions } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { split } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';
import { getMainDefinition, variablesInOperation } from 'apollo-utilities';
import gql from 'graphql-tag';
import { ProfileChanges } from '../store/actions/profile';
export class UnauthorizedError extends Error {
constructor(...args: any[]) {
@ -54,10 +55,22 @@ export class DaddyClient {
wsLink,
httpLink,
);
const defaultOptions: DefaultOptions = {
watchQuery: {
fetchPolicy: 'no-cache',
errorPolicy: 'ignore',
},
query: {
fetchPolicy: 'no-cache',
errorPolicy: 'all',
},
};
this.gql = new ApolloClient<any>({
link: link,
cache: new InMemoryCache(),
defaultOptions,
});
}
@ -66,6 +79,7 @@ export class DaddyClient {
query: gql`
query {
userProfile {
name,
email,
createdAt,
connectedAt
@ -75,6 +89,24 @@ export class DaddyClient {
.then(this.assertAuthorization)
}
updateProfile(changes: ProfileChanges) {
return this.gql.mutate({
variables: {
changes,
},
mutation: gql`
mutation updateProfile($changes: ProfileChanges!) {
updateProfile(changes: $changes) {
name,
email,
createdAt,
connectedAt
}
}`,
})
.then(this.assertAuthorization)
}
assertAuthorization({ status, data }: any) {
if (status === 401) return Promise.reject(new UnauthorizedError());
return data;

View File

@ -3,17 +3,16 @@ package main
import (
"context"
"net/http"
"time"
"forge.cadoles.com/Cadoles/daddy/internal/migration"
"gitlab.com/wpetit/goweb/cqrs"
"github.com/wader/gormstore"
"forge.cadoles.com/Cadoles/daddy/internal/database"
"forge.cadoles.com/Cadoles/daddy/internal/orm"
"gitlab.com/wpetit/goweb/logger"
"forge.cadoles.com/Cadoles/daddy/internal/config"
oidc "forge.cadoles.com/wpetit/goweb-oidc"
"github.com/gorilla/sessions"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
"gitlab.com/wpetit/goweb/service/build"
@ -51,23 +50,36 @@ func getServiceContainer(ctx context.Context, conf *config.Config) (*service.Con
conf.HTTP.CookieEncryptionKey = string(cookieEncryptionKey)
}
ctn.Provide(orm.ServiceName, orm.ServiceProvider("postgres", conf.Database.DSN, conf.Debug))
orm, err := orm.From(ctn)
if err != nil {
return nil, errors.WithStack(err)
}
// Create and initialize HTTP session service provider
cookieStore := sessions.NewCookieStore(
sessionStore := gormstore.NewOptions(
orm.DB(),
gormstore.Options{
TableName: "sessions",
SkipCreateTable: false,
},
[]byte(conf.HTTP.CookieAuthenticationKey),
[]byte(conf.HTTP.CookieEncryptionKey),
)
quit := make(chan struct{})
go sessionStore.PeriodicCleanup(1*time.Hour, quit)
// Define default cookie options
cookieStore.Options = &sessions.Options{
Path: "/",
HttpOnly: true,
MaxAge: conf.HTTP.CookieMaxAge,
SameSite: http.SameSiteStrictMode,
}
sessionStore.SessionOpts.Path = "/"
sessionStore.SessionOpts.HttpOnly = true
sessionStore.SessionOpts.MaxAge = conf.HTTP.CookieMaxAge
sessionStore.SessionOpts.SameSite = http.SameSiteStrictMode
ctn.Provide(
session.ServiceName,
gorilla.ServiceProvider("daddy", cookieStore),
gorilla.ServiceProvider("daddy", sessionStore),
)
// Create and expose config service provider
@ -84,21 +96,5 @@ func getServiceContainer(ctx context.Context, conf *config.Config) (*service.Con
oidc.WithScopes("email", "openid"),
))
ctn.Provide(database.ServiceName, database.ServiceProvider(conf.Database.DSN))
dbpool, err := database.From(ctn)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve database service")
}
versionResolver := database.NewVersionResolver(dbpool)
if err := versionResolver.Init(ctx); err != nil {
return nil, errors.Wrap(err, "could not initialize database version resolver")
}
ctn.Provide(migration.ServiceName, migration.ServiceProvider(versionResolver))
ctn.Provide(cqrs.ServiceName, cqrs.ServiceProvider())
return ctn, nil
}

View File

@ -1,37 +0,0 @@
package main
import (
"forge.cadoles.com/Cadoles/daddy/internal/command"
"forge.cadoles.com/Cadoles/daddy/internal/query"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/cqrs"
"gitlab.com/wpetit/goweb/service"
)
func initCommands(ctn *service.Container) error {
dispatcher, err := cqrs.From(ctn)
if err != nil {
return errors.WithStack(err)
}
dispatcher.RegisterCommand(
cqrs.MatchCommandRequest(&command.CreateUserCommandRequest{}),
cqrs.CommandHandlerFunc(command.HandleCreateUserCommand),
)
return nil
}
func initQueries(ctn *service.Container) error {
dispatcher, err := cqrs.From(ctn)
if err != nil {
return errors.WithStack(err)
}
dispatcher.RegisterQuery(
cqrs.MatchQueryRequest(&query.FindUserQueryRequest{}),
cqrs.QueryHandlerFunc(query.HandleFindUserQuery),
)
return nil
}

View File

@ -153,23 +153,6 @@ func main() {
os.Exit(0)
}
// Init commands and queries
if err := initCommands(ctn); err != nil {
logger.Fatal(
ctx,
"could not init commands",
logger.E(err),
)
}
if err := initQueries(ctn); err != nil {
logger.Fatal(
ctx,
"could not init queries",
logger.E(err),
)
}
r := chi.NewRouter()
// Define base middlewares

View File

@ -3,9 +3,9 @@ package main
import (
"context"
"forge.cadoles.com/Cadoles/daddy/internal/database"
"forge.cadoles.com/Cadoles/daddy/internal/migration"
"github.com/jackc/pgx/v4"
"forge.cadoles.com/Cadoles/daddy/internal/model"
"forge.cadoles.com/Cadoles/daddy/internal/orm"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
"gitlab.com/wpetit/goweb/service"
@ -18,11 +18,13 @@ const (
)
func applyMigration(ctx context.Context, ctn *service.Container) error {
migr, err := migration.From(ctn)
orm, err := orm.From(ctn)
if err != nil {
return err
}
migr := orm.Migration()
// Register available migrations
migr.Register(
m000initialSchema(),
@ -74,29 +76,31 @@ func applyMigration(ctx context.Context, ctn *service.Container) error {
return nil
}
func m000initialSchema() migration.Migration {
return database.NewMigration(
// nolint: gochecknoglobals
var initialModels = []interface{}{
&model.User{},
}
func m000initialSchema() orm.Migration {
return orm.NewDBMigration(
"00_initial_schema",
func(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx, `
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT,
email TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
connected_at TIMESTAMPTZ,
CONSTRAINT unique_email unique(email)
);
`)
func(ctx context.Context, tx *gorm.DB) error {
for _, m := range initialModels {
if err := tx.AutoMigrate(m).Error; err != nil {
return errors.WithStack(err)
}
}
return err
return nil
},
func(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx, `
DROP TABLE users;
`)
func(ctx context.Context, tx *gorm.DB) error {
for _, m := range initialModels {
if err := tx.DropTableIfExists(m).Error; err != nil {
return errors.WithStack(err)
}
}
return err
return nil
},
)
}

1
debian/compat vendored Normal file
View File

@ -0,0 +1 @@
9

14
debian/control vendored Normal file
View File

@ -0,0 +1,14 @@
Source: daddy
Section: unknown
Priority: optional
Maintainer: Cadoles <contact@cadoles.com>
Build-Depends: debhelper (>= 8.0.0), wget, ca-certificates, tar
Standards-Version: 3.9.4
Homepage: http://forge.cadoles.com/Cadoles/daddy
Vcs-Git: http://forge.cadoles.com/Cadoles/daddy.git
Vcs-Browser: http://forge.cadoles.com/Cadoles/daddy
Package: daddy
Architecture: amd64
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: Daddy app

1
debian/daddy.links vendored Normal file
View File

@ -0,0 +1 @@
/etc/daddy/client-config.js /usr/share/daddy/public/config.js

11
debian/daddy.service vendored Normal file
View File

@ -0,0 +1,11 @@
[Unit]
Description=Daddy app
After=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/daddy -workdir /usr/share/daddy -config /etc/daddy/config.yml
Restart=on-failure
[Install]
WantedBy=multi-user.target

56
debian/rules vendored Normal file
View File

@ -0,0 +1,56 @@
#!/usr/bin/make -f
# -*- makefile -*-
# Uncomment this to turn on verbose mode.
export DH_VERBOSE=1
GO_VERSION := 1.13.5
OS := linux
ARCH := amd64
GOPATH=$(HOME)/go
ifeq (, $(shell which go 2>/dev/null))
override_dh_auto_build: install-go
override_dh_auto_clean: install-go
endif
ifeq (, $(shell which node 2>/dev/null))
override_dh_auto_build: install-nodejs
endif
%:
dh $@ --with systemd
override_dh_auto_build: $(GOPATH)
GOPATH=$(GOPATH) PATH="$(PATH):/usr/local/go/bin:$(GOPATH)/bin" make deps
GOPATH=$(GOPATH) PATH="$(PATH):/usr/local/go/bin:$(GOPATH)/bin" ARCH_TARGETS=$(ARCH) make release
$(GOPATH):
mkdir -p $(GOPATH)
install-go:
wget -nc https://dl.google.com/go/go$(GO_VERSION).$(OS)-$(ARCH).tar.gz
tar -C /usr/local -xzf go$(GO_VERSION).$(OS)-$(ARCH).tar.gz
install-nodejs:
wget -O- https://deb.nodesource.com/setup_12.x | bash -
apt-get install -y nodejs
override_dh_auto_install:
mkdir -p debian/daddy/usr/share/daddy
mkdir -p debian/daddy/etc/daddy
mkdir -p debian/daddy/usr/bin
cp -r release/server-$(OS)-$(ARCH)/* debian/daddy/usr/share/daddy/
mv debian/daddy/usr/share/daddy/bin/server debian/daddy/usr/bin/daddy
mv debian/daddy/usr/share/daddy/server.conf debian/daddy/etc/daddy/config.yml
mv debian/daddy/usr/share/daddy/public/config.js debian/daddy/etc/daddy/client-config.js
install -d debian/daddy
override_dh_strip:
override_dh_auto_test:

1
debian/source/format vendored Normal file
View File

@ -0,0 +1 @@
3.0 (native)

3
go.mod
View File

@ -7,14 +7,17 @@ require (
github.com/99designs/gqlgen v0.11.3
github.com/caarlos0/env/v6 v6.2.2
github.com/cortesi/modd v0.0.0-20200630120222-8983974e5450 // indirect
github.com/davecgh/go-spew v1.1.1
github.com/go-chi/chi v4.1.0+incompatible
github.com/gorilla/sessions v1.2.0
github.com/gorilla/websocket v1.2.0
github.com/jackc/pgx v3.6.2+incompatible
github.com/jackc/pgx/v4 v4.7.1
github.com/jinzhu/gorm v1.9.14
github.com/pkg/errors v0.9.1
github.com/rs/cors v1.7.0
github.com/vektah/gqlparser/v2 v2.0.1
github.com/wader/gormstore v0.0.0-20200328121358-65a111a20c23
gitlab.com/wpetit/goweb v0.0.0-20200707070104-985ce3eba3c2
gopkg.in/yaml.v2 v2.2.8
)

36
go.sum
View File

@ -22,6 +22,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
@ -42,6 +43,7 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/bmatcuk/doublestar v1.3.0/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/bmatcuk/doublestar v1.3.1 h1:rT8rxDPsavp9G+4ZULzqhhUSaI/OPsTZNG88Z3i0xvY=
@ -50,6 +52,7 @@ github.com/caarlos0/env/v6 v6.2.2 h1:R0NIFXaB/LhwuGrjnsldzpnVNjFU/U+hTVHt+cq0yDY
github.com/caarlos0/env/v6 v6.2.2/go.mod h1:3LpmfcAYCG6gCiSgDLaFR5Km1FRpPwFvBbRcjHar6Sw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
@ -73,12 +76,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs=
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
@ -90,9 +97,15 @@ github.com/go-chi/chi v4.1.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxm
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -121,6 +134,8 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@ -188,6 +203,13 @@ github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv
github.com/jackc/puddle v1.1.1 h1:PJAw7H/9hoWC4Kf3J8iNmL1SwA6E8vfsLqBiL+F6CtI=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
github.com/jinzhu/gorm v1.9.14 h1:Kg3ShyTPcM6nzVo148fRrcMO6MNKuqtOUwnzqMgVniM=
github.com/jinzhu/gorm v1.9.14/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -203,7 +225,9 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
@ -226,6 +250,10 @@ github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGe
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@ -260,6 +288,7 @@ github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
@ -284,6 +313,8 @@ github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWp
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o=
github.com/vektah/gqlparser/v2 v2.0.1/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
github.com/wader/gormstore v0.0.0-20200328121358-65a111a20c23 h1:gtfR002LWpH9vQ1/GLbWBOTcS92cBi5PAR021lArKF8=
github.com/wader/gormstore v0.0.0-20200328121358-65a111a20c23/go.mod h1:2z7nYWeR0xUeFNCmlyH6Qt6qigF+Kl/k4LbQbj6Ksus=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
gitlab.com/wpetit/goweb v0.0.0-20200418152305-76dea96a46ce h1:B3inZUHFr/FpA3jb+ZeSSHk3FSpB0xkQ0TjePhRokxw=
gitlab.com/wpetit/goweb v0.0.0-20200418152305-76dea96a46ce/go.mod h1:Gfv7cBOw1T2XwXMsLm1d9kAjMAdNtLMjPv+yCzRO9qk=
@ -302,6 +333,7 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -309,6 +341,7 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -333,6 +366,7 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCc
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -347,6 +381,8 @@ golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=

3
internal/.gitignore vendored
View File

@ -1,2 +1,3 @@
/server.go
/graph/generated
/graph/generated
/model/models_gen.go

View File

@ -1,99 +0,0 @@
package command
import (
"context"
"github.com/jackc/pgx/v4/pgxpool"
"forge.cadoles.com/Cadoles/daddy/internal/database"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/cqrs"
"gitlab.com/wpetit/goweb/middleware/container"
)
const (
createConnectedUserStatement = `
INSERT INTO users (email, connected_at) VALUES ($1, now())
ON CONFLICT ON CONSTRAINT unique_email
DO UPDATE SET connected_at = now();
`
createUserStatement = `
INSERT INTO users (email) VALUES ($1)
ON CONFLICT ON CONSTRAINT unique_email
DO NOTHING;
`
)
type CreateUserCommandRequest struct {
Email string
Connected bool
}
func HandleCreateUserCommand(ctx context.Context, cmd cqrs.Command) error {
req, ok := cmd.Request().(*CreateUserCommandRequest)
if !ok {
return errors.WithStack(cqrs.ErrUnexpectedRequest)
}
ctn, err := container.From(ctx)
if err != nil {
return errors.WithStack(err)
}
pool, err := database.From(ctn)
if err != nil {
return errors.WithStack(err)
}
conn, err := pool.Acquire(ctx)
if err != nil {
return errors.WithStack(err)
}
defer conn.Release()
if req.Connected {
if err := createConnectedUser(ctx, conn, req.Email); err != nil {
return errors.WithStack(err)
}
} else {
if err := createUser(ctx, conn, req.Email); err != nil {
return errors.WithStack(err)
}
}
return nil
}
func createConnectedUser(ctx context.Context, conn *pgxpool.Conn, email string) error {
_, err := conn.Conn().Prepare(
ctx, "create_connected_user",
createConnectedUserStatement,
)
if err != nil {
return errors.WithStack(err)
}
if _, err := conn.Exec(ctx, "create_connected_user", email); err != nil {
return errors.WithStack(err)
}
return nil
}
func createUser(ctx context.Context, conn *pgxpool.Conn, email string) error {
_, err := conn.Conn().Prepare(
ctx, "create_user",
createUserStatement,
)
if err != nil {
return errors.WithStack(err)
}
if _, err := conn.Exec(ctx, "create_user", email); err != nil {
return errors.WithStack(err)
}
return nil
}

View File

@ -55,7 +55,7 @@ type CORSConfig struct {
type OIDCConfig struct {
ClientID string `yaml:"clientId" env:"OIDC_CLIENT_ID"`
ClientSecret string `yaml:"clientSecret" env:"OIDC_CLIENT_SECRET"`
IssuerURL string `ymal:"issuerUrl" env:"OIDC_ISSUER_URL"`
IssuerURL string `yaml:"issuerUrl" env:"OIDC_ISSUER_URL"`
RedirectURL string `yaml:"redirectUrl" env:"OIDC_REDIRECT_URL"`
PostLogoutRedirectURL string `yaml:"postLogoutRedirectURL" env:"OIDC_POST_LOGOUT_REDIRECT_URL"`
}

View File

@ -1,79 +0,0 @@
package database
import (
"context"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/middleware/container"
)
type MigrationFunc func(ctx context.Context, tx pgx.Tx) error
type Migration struct {
version string
up MigrationFunc
down MigrationFunc
}
func (m *Migration) Version() string {
return m.version
}
func (m *Migration) Up(ctx context.Context) error {
pool, err := m.getDatabaseService(ctx)
if err != nil {
return err
}
err = WithTx(ctx, pool, func(ctx context.Context, tx pgx.Tx) error {
return m.up(ctx, tx)
})
if err != nil {
return errors.Wrap(err, "could not apply up migration")
}
return nil
}
func (m *Migration) Down(ctx context.Context) error {
pool, err := m.getDatabaseService(ctx)
if err != nil {
return err
}
err = WithTx(ctx, pool, func(ctx context.Context, tx pgx.Tx) error {
return m.down(ctx, tx)
})
if err != nil {
return errors.Wrap(err, "could not apply down migration")
}
return nil
}
func (m *Migration) getDatabaseService(ctx context.Context) (*pgxpool.Pool, error) {
ctn, err := container.From(ctx)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve service container")
}
pool, err := From(ctn)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve database service")
}
return pool, nil
}
func NewMigration(version string, up, down MigrationFunc) *Migration {
return &Migration{
version: version,
up: up,
down: down,
}
}

View File

@ -1,24 +0,0 @@
package database
import (
"context"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
)
func ServiceProvider(dsn string) service.Provider {
pool, err := pgxpool.Connect(context.Background(), dsn)
if err != nil {
err = errors.Wrap(err, "could not connect to database")
}
return func(ctn *service.Container) (interface{}, error) {
if err != nil {
return nil, err
}
return pool, nil
}
}

View File

@ -1,34 +0,0 @@
package database
import (
"github.com/jackc/pgx/v4/pgxpool"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
)
const ServiceName service.Name = "database"
// From retrieves the database pool service in the given container.
func From(container *service.Container) (*pgxpool.Pool, error) {
service, err := container.Service(ServiceName)
if err != nil {
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
}
srv, ok := service.(*pgxpool.Pool)
if !ok {
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
}
return srv, nil
}
// Must retrieves the database pool service in the given container or panic otherwise.
func Must(container *service.Container) *pgxpool.Pool {
srv, err := From(container)
if err != nil {
panic(err)
}
return srv
}

View File

@ -1,38 +0,0 @@
package database
import (
"context"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/pkg/errors"
)
func WithTx(ctx context.Context, pool *pgxpool.Pool, fn func(context.Context, pgx.Tx) error) error {
tx, err := pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return errors.Wrap(err, "could not begin transaction")
}
defer func() {
if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) {
panic(errors.Wrap(err, "could not rollback transaction"))
}
}()
if err := fn(ctx, tx); err != nil {
err := errors.Wrap(err, "could not apply down migration")
if rollbackErr := tx.Rollback(ctx); rollbackErr != nil {
return errors.Wrap(err, rollbackErr.Error())
}
return err
}
if err := tx.Commit(ctx); err != nil {
return errors.Wrap(err, "could not commit transaction")
}
return nil
}

View File

@ -1,94 +0,0 @@
package database
import (
"context"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/pkg/errors"
)
type VersionResolver struct {
pool *pgxpool.Pool
}
func (r *VersionResolver) Current(ctx context.Context) (string, error) {
var version string
err := WithTx(ctx, r.pool, func(ctx context.Context, tx pgx.Tx) error {
err := tx.QueryRow(ctx, `SELECT version FROM database_schema WHERE is_current = true;`).
Scan(&version)
if errors.Is(err, pgx.ErrNoRows) {
return nil
}
return err
})
if err != nil {
return "", errors.Wrap(err, "could execute version resolver init transaction")
}
return version, nil
}
func (r *VersionResolver) Set(ctx context.Context, version string) error {
err := WithTx(ctx, r.pool, func(ctx context.Context, tx pgx.Tx) error {
if version != "" {
_, err := tx.Exec(ctx, `
INSERT INTO database_schema (version, is_current, migrated_at)
VALUES
(
$1,
true,
now()
)
ON CONFLICT ON CONSTRAINT unique_version
DO UPDATE SET migrated_at = now(), is_current = true;
`, version)
if err != nil {
return err
}
}
_, err := tx.Exec(ctx, `
UPDATE database_schema SET is_current = false, migrated_at = null WHERE version <> $1;
`, version)
return err
})
if err != nil {
return errors.Wrap(err, "could not update schema version")
}
return nil
}
func (r *VersionResolver) Init(ctx context.Context) error {
err := WithTx(ctx, r.pool, func(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx, `
CREATE TABLE IF NOT EXISTS database_schema(
version TEXT NOT NULL,
migrated_at TIME,
is_current BOOLEAN,
CONSTRAINT unique_version UNIQUE(version)
);`)
return err
})
if err != nil {
return errors.Wrap(err, "could execute version resolver init transaction")
}
return nil
}
func NewVersionResolver(pool *pgxpool.Pool) *VersionResolver {
return &VersionResolver{
pool: pool,
}
}

View File

@ -1,6 +1,6 @@
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
schema:
- graph/*.graphqls
- graph/*.graphql
# Where should the generated server code go?
exec:
@ -14,7 +14,7 @@ exec:
# Where should any generated models go?
model:
filename: graph/model/models_gen.go
filename: model/models_gen.go
package: model
# Where should the resolver implementations go?
@ -35,7 +35,7 @@ resolver:
# gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them.
autobind:
- "forge.cadoles.com/Cadoles/daddy/internal/graph/model"
- "forge.cadoles.com/Cadoles/daddy/internal/model"
# This section declares type mapping between the GraphQL and go type systems
#

25
internal/graph/helper.go Normal file
View File

@ -0,0 +1,25 @@
package graph
import (
"context"
"forge.cadoles.com/Cadoles/daddy/internal/orm"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/middleware/container"
)
func getDB(ctx context.Context) (*gorm.DB, error) {
ctn, err := container.From(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
orm, err := orm.From(ctn)
if err != nil {
return nil, errors.WithStack(err)
}
return orm.DB(), nil
}

View File

@ -1,14 +0,0 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package model
import (
"time"
)
type User struct {
Name *string `json:"name"`
Email string `json:"email"`
ConnectedAt time.Time `json:"connectedAt"`
CreatedAt time.Time `json:"createdAt"`
}

View File

@ -0,0 +1,7 @@
input ProfileChanges {
name: String
}
type Mutation {
updateProfile(changes: ProfileChanges!): User!
}

View File

@ -0,0 +1,20 @@
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"forge.cadoles.com/Cadoles/daddy/internal/graph/generated"
"forge.cadoles.com/Cadoles/daddy/internal/model"
)
func (r *mutationResolver) UpdateProfile(ctx context.Context, changes model.ProfileChanges) (*model.User, error) {
return handleUpdateUserProfile(ctx, changes)
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
type mutationResolver struct{ *Resolver }

View File

@ -1,7 +1,3 @@
# GraphQL schema example
#
# https://gqlgen.com/getting-started/
scalar Time
type User {
@ -13,4 +9,4 @@ type User {
type Query {
userProfile: User
}
}

View File

@ -7,10 +7,10 @@ import (
"context"
"forge.cadoles.com/Cadoles/daddy/internal/graph/generated"
"forge.cadoles.com/Cadoles/daddy/internal/graph/model"
model1 "forge.cadoles.com/Cadoles/daddy/internal/model"
)
func (r *queryResolver) UserProfile(ctx context.Context) (*model.User, error) {
func (r *queryResolver) UserProfile(ctx context.Context) (*model1.User, error) {
return handleUserProfile(ctx)
}

View File

@ -4,4 +4,6 @@ package graph
//
// It serves as dependency injection for your app, add any dependencies you require here.
//go:generate go run github.com/99designs/gqlgen
type Resolver struct{}

View File

@ -3,43 +3,56 @@ package graph
import (
"context"
"forge.cadoles.com/Cadoles/daddy/internal/graph/model"
"forge.cadoles.com/Cadoles/daddy/internal/query"
"forge.cadoles.com/Cadoles/daddy/internal/session"
"forge.cadoles.com/Cadoles/daddy/internal/model"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/cqrs"
"gitlab.com/wpetit/goweb/middleware/container"
)
func handleUserProfile(ctx context.Context) (*model.User, error) {
db, err := getDB(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
userEmail, err := session.UserEmail(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
ctn, err := container.From(ctx)
repo := model.NewUserRepository(db)
user, err := repo.FindUserByEmail(ctx, userEmail)
if err != nil {
return nil, errors.WithStack(err)
}
dispatcher, err := cqrs.From(ctn)
if err != nil {
return nil, errors.WithStack(err)
}
qry := &query.FindUserQueryRequest{
Email: userEmail,
}
result, err := dispatcher.Query(ctx, qry)
if err != nil {
return nil, errors.WithStack(err)
}
findUserData, ok := result.Data().(*query.FindUserData)
if !ok {
return nil, errors.WithStack(cqrs.ErrUnexpectedData)
}
return findUserData.User, nil
return user, nil
}
func handleUpdateUserProfile(ctx context.Context, changes model.ProfileChanges) (*model.User, error) {
db, err := getDB(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
userEmail, err := session.UserEmail(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
repo := model.NewUserRepository(db)
userChanges := &model.User{}
if changes.Name != nil {
userChanges.Name = changes.Name
}
user, err := repo.UpdateUserByEmail(ctx, userEmail, userChanges)
if err != nil {
return nil, errors.WithStack(err)
}
return user, nil
}

View File

@ -1,9 +0,0 @@
package migration
import "context"
type Migration interface {
Version() string
Up(context.Context) error
Down(context.Context) error
}

View File

@ -1,13 +0,0 @@
package migration
import (
"gitlab.com/wpetit/goweb/service"
)
func ServiceProvider(resolver VersionResolver) service.Provider {
manager := NewManager(resolver)
return func(ctn *service.Container) (interface{}, error) {
return manager, nil
}
}

View File

@ -1,33 +0,0 @@
package migration
import (
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
)
const ServiceName service.Name = "migration"
// From retrieves the migration service in the given container.
func From(container *service.Container) (*Manager, error) {
service, err := container.Service(ServiceName)
if err != nil {
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
}
srv, ok := service.(*Manager)
if !ok {
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
}
return srv, nil
}
// Must retrieves the migration service in the given container or panic otherwise.
func Must(container *service.Container) *Manager {
srv, err := From(container)
if err != nil {
panic(err)
}
return srv
}

View File

@ -1,8 +0,0 @@
package migration
import "context"
type VersionResolver interface {
Current(context.Context) (string, error)
Set(context.Context, string) error
}

15
internal/model/user.go Normal file
View File

@ -0,0 +1,15 @@
package model
import "time"
type User struct {
ID *uint `gorm:"primary_key"`
Name *string `json:"name"`
Email string `json:"email" gorm:"unique;not null"`
ConnectedAt time.Time `json:"connectedAt"`
CreatedAt time.Time `json:"createdAt"`
}
type ProfileChanges struct {
Name *string `json:"name"`
}

View File

@ -0,0 +1,74 @@
package model
import (
"context"
"time"
"forge.cadoles.com/Cadoles/daddy/internal/orm"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
)
type UserRepository struct {
db *gorm.DB
}
func (r *UserRepository) CreateOrConnectUser(ctx context.Context, email string) (*User, error) {
user := &User{
Email: email,
CreatedAt: time.Now(),
}
err := orm.WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
err := tx.Where("email = ?", email).FirstOrCreate(user).Error
if err != nil {
return errors.WithStack(err)
}
if err := tx.Model(user).UpdateColumn("connected_at", time.Now()).Error; err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return nil, errors.Wrap(err, "could not create user")
}
return user, nil
}
func (r *UserRepository) FindUserByEmail(ctx context.Context, email string) (*User, error) {
user := &User{
Email: email,
}
err := r.db.First(user, "email = ?", email).Error
if err != nil {
return nil, errors.Wrap(err, "could not find user")
}
return user, nil
}
func (r *UserRepository) UpdateUserByEmail(ctx context.Context, email string, changes *User) (*User, error) {
user := &User{
Email: email,
}
err := r.db.First(user, "email = ?", email).Error
if err != nil {
return nil, errors.Wrap(err, "could not find user")
}
if err := r.db.Model(user).Updates(changes).Error; err != nil {
return nil, errors.Wrap(err, "could not update user")
}
return user, nil
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db}
}

84
internal/orm/migration.go Normal file
View File

@ -0,0 +1,84 @@
package orm
import (
"context"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/middleware/container"
)
type MigrationFunc func(ctx context.Context, tx *gorm.DB) error
type Migration interface {
Version() string
Up(context.Context) error
Down(context.Context) error
}
type DBMigration struct {
version string
up MigrationFunc
down MigrationFunc
}
func (m *DBMigration) Version() string {
return m.version
}
func (m *DBMigration) Up(ctx context.Context) error {
db, err := m.getDatabase(ctx)
if err != nil {
return err
}
err = WithTx(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
return m.up(ctx, tx)
})
if err != nil {
return errors.Wrap(err, "could not apply up migration")
}
return nil
}
func (m *DBMigration) Down(ctx context.Context) error {
db, err := m.getDatabase(ctx)
if err != nil {
return err
}
err = WithTx(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
return m.down(ctx, tx)
})
if err != nil {
return errors.Wrap(err, "could not apply down migration")
}
return nil
}
func (m *DBMigration) getDatabase(ctx context.Context) (*gorm.DB, error) {
ctn, err := container.From(ctx)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve service container")
}
orm, err := From(ctn)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve orm service")
}
return orm.DB(), nil
}
func NewDBMigration(version string, up, down MigrationFunc) *DBMigration {
return &DBMigration{
version: version,
up: up,
down: down,
}
}

View File

@ -1,4 +1,4 @@
package migration
package orm
import (
"context"
@ -11,12 +11,12 @@ var (
ErrMigrationNotFound = errors.New("migration not found")
)
type Manager struct {
type MigrationManager struct {
migrations []Migration
resolver VersionResolver
}
func (m *Manager) Up(ctx context.Context) error {
func (m *MigrationManager) Up(ctx context.Context) error {
currentVersion, err := m.resolver.Current(ctx)
if err != nil {
return errors.Wrap(err, "could not retrieve current version")
@ -58,7 +58,7 @@ func (m *Manager) Up(ctx context.Context) error {
return errors.WithStack(ErrMigrationNotFound)
}
func (m *Manager) Down(ctx context.Context) error {
func (m *MigrationManager) Down(ctx context.Context) error {
currentVersion, err := m.resolver.Current(ctx)
if err != nil {
return errors.Wrap(err, "could not retrieve current version")
@ -91,7 +91,7 @@ func (m *Manager) Down(ctx context.Context) error {
return errors.WithStack(ErrMigrationNotFound)
}
func (m *Manager) Latest(ctx context.Context) error {
func (m *MigrationManager) Latest(ctx context.Context) error {
for {
isLatest, err := m.IsLatest(ctx)
if err != nil {
@ -108,15 +108,15 @@ func (m *Manager) Latest(ctx context.Context) error {
}
}
func (m *Manager) Register(migrations ...Migration) {
func (m *MigrationManager) Register(migrations ...Migration) {
m.migrations = migrations
}
func (m *Manager) CurrentVersion(ctx context.Context) (string, error) {
func (m *MigrationManager) CurrentVersion(ctx context.Context) (string, error) {
return m.resolver.Current(ctx)
}
func (m *Manager) LatestVersion() (string, error) {
func (m *MigrationManager) LatestVersion() (string, error) {
if len(m.migrations) == 0 {
return "", errors.WithStack(ErrNoAvailableMigration)
}
@ -124,7 +124,7 @@ func (m *Manager) LatestVersion() (string, error) {
return m.migrations[len(m.migrations)-1].Version(), nil
}
func (m *Manager) IsLatest(ctx context.Context) (bool, error) {
func (m *MigrationManager) IsLatest(ctx context.Context) (bool, error) {
currentVersion, err := m.resolver.Current(ctx)
if err != nil {
return false, errors.Wrap(err, "could not retrieve current version")
@ -138,8 +138,8 @@ func (m *Manager) IsLatest(ctx context.Context) (bool, error) {
return currentVersion == latestVersion, nil
}
func NewManager(resolver VersionResolver) *Manager {
return &Manager{
func NewMigrationManager(resolver VersionResolver) *MigrationManager {
return &MigrationManager{
resolver: resolver,
migrations: make([]Migration, 0),
}

49
internal/orm/provider.go Normal file
View File

@ -0,0 +1,49 @@
package orm
import (
"context"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
// Import postgres dialect
_ "github.com/jinzhu/gorm/dialects/postgres"
)
func ServiceProvider(dialect, dsn string, debug bool) service.Provider {
db, err := gorm.Open(dialect, dsn)
if err != nil {
err = errors.Wrap(err, "could not connect to database")
}
var srv *Service
if err == nil {
db = db.LogMode(debug)
versionResolver := NewDBVersionResolver(db)
ctx := context.Background()
err := versionResolver.Init(ctx)
if err != nil {
err = errors.Wrap(err, "could not initialize version resolver")
}
if err == nil {
srv = &Service{
db: db,
migration: NewMigrationManager(versionResolver),
}
}
}
return func(ctn *service.Container) (interface{}, error) {
if err != nil {
return nil, err
}
return srv, nil
}
}

47
internal/orm/service.go Normal file
View File

@ -0,0 +1,47 @@
package orm
import (
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
)
const ServiceName service.Name = "orm"
type Service struct {
db *gorm.DB
migration *MigrationManager
}
func (s *Service) DB() *gorm.DB {
return s.db
}
func (s *Service) Migration() *MigrationManager {
return s.migration
}
// From retrieves the orm service in the given container.
func From(container *service.Container) (*Service, error) {
service, err := container.Service(ServiceName)
if err != nil {
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
}
srv, ok := service.(*Service)
if !ok {
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
}
return srv, nil
}
// Must retrieves the orm pool service in the given container or panic otherwise.
func Must(container *service.Container) *Service {
srv, err := From(container)
if err != nil {
panic(err)
}
return srv
}

47
internal/orm/tx.go Normal file
View File

@ -0,0 +1,47 @@
package orm
import (
"context"
"database/sql"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
)
func WithTx(ctx context.Context, db *gorm.DB, fn func(context.Context, *gorm.DB) error) error {
tx := db.BeginTx(ctx, &sql.TxOptions{})
defer func() {
if err := tx.Rollback().Error; err != nil && !isGormError(err, gorm.ErrInvalidTransaction) {
panic(errors.Wrap(err, "could not rollback transaction"))
}
}()
if err := fn(ctx, tx); err != nil {
err := errors.Wrap(err, "could not apply down migration")
if rollbackErr := tx.Rollback().Error; rollbackErr != nil {
return errors.Wrap(err, rollbackErr.Error())
}
return err
}
if err := tx.Commit().Error; err != nil {
return errors.Wrap(err, "could not commit transaction")
}
return nil
}
func isGormError(err error, compErr error) bool {
if errs, ok := err.(gorm.Errors); ok {
for _, err := range errs {
if errors.Is(err, compErr) {
return true
}
}
}
return errors.Is(err, compErr)
}

View File

@ -0,0 +1,112 @@
package orm
import (
"context"
"time"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
)
type VersionResolver interface {
Current(context.Context) (string, error)
Set(context.Context, string) error
}
type DBVersionResolver struct {
db *gorm.DB
}
type DatabaseVersion struct {
ID uint `gorm:"primary_key"`
Version string `gorm:"unique; not null"`
MigratedAt time.Time
IsCurrent bool
}
func (r *DBVersionResolver) Current(ctx context.Context) (string, error) {
var version string
err := WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
dbVersion := &DatabaseVersion{}
err := tx.Where("is_current = ?", true).First(dbVersion).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return errors.WithStack(err)
}
version = dbVersion.Version
return nil
})
if err != nil {
return "", errors.Wrap(err, "could execute version resolver init transaction")
}
return version, nil
}
func (r *DBVersionResolver) Set(ctx context.Context, version string) error {
err := WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
dbVersion := &DatabaseVersion{
Version: version,
MigratedAt: time.Now(),
}
if version != "" {
if err := tx.FirstOrCreate(dbVersion).Error; err != nil {
return err
}
err := tx.Model(dbVersion).
UpdateColumn("is_current", true).Error
if err != nil {
return errors.WithStack(err)
}
}
err := tx.Model(&DatabaseVersion{}).
Where("version <> ?", version).
UpdateColumn("is_current", false).Error
if err != nil {
return errors.WithStack(err)
}
return err
})
if err != nil {
return errors.Wrap(err, "could not update schema version")
}
return nil
}
func (r *DBVersionResolver) Init(ctx context.Context) error {
err := WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
if err := tx.AutoMigrate(&DatabaseVersion{}).Error; err != nil {
return errors.WithStack(err)
}
if err := tx.Model(&DatabaseVersion{}).AddUniqueIndex("idx_unique_version", "version").Error; err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return errors.Wrap(err, "could execute version resolver init transaction")
}
return nil
}
func NewDBVersionResolver(db *gorm.DB) *DBVersionResolver {
return &DBVersionResolver{
db: db,
}
}

View File

@ -1,71 +0,0 @@
package query
import (
"context"
"forge.cadoles.com/Cadoles/daddy/internal/graph/model"
"forge.cadoles.com/Cadoles/daddy/internal/database"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/cqrs"
"gitlab.com/wpetit/goweb/middleware/container"
)
const (
findUserStatement = `SELECT email, connected_at, created_at FROM users WHERE email = $1`
)
type FindUserQueryRequest struct {
Email string
}
type FindUserData struct {
User *model.User
}
func HandleFindUserQuery(ctx context.Context, qry cqrs.Query) (interface{}, error) {
req, ok := qry.Request().(*FindUserQueryRequest)
if !ok {
return nil, errors.WithStack(cqrs.ErrUnexpectedRequest)
}
ctn, err := container.From(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
pool, err := database.From(ctn)
if err != nil {
return nil, errors.WithStack(err)
}
conn, err := pool.Acquire(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
defer conn.Release()
_, err = conn.Conn().Prepare(
ctx, "find_user",
findUserStatement,
)
if err != nil {
return nil, errors.WithStack(err)
}
user := &model.User{}
err = conn.QueryRow(ctx, "find_user", req.Email).
Scan(&user.Email, &user.ConnectedAt, &user.CreatedAt)
if err != nil {
return nil, errors.WithStack(err)
}
data := &FindUserData{
User: user,
}
return data, nil
}

View File

@ -3,8 +3,8 @@ package route
import (
"net/http"
"forge.cadoles.com/Cadoles/daddy/internal/command"
"gitlab.com/wpetit/goweb/cqrs"
"forge.cadoles.com/Cadoles/daddy/internal/model"
"forge.cadoles.com/Cadoles/daddy/internal/orm"
"forge.cadoles.com/Cadoles/daddy/internal/session"
@ -62,15 +62,11 @@ func handleLoginCallback(w http.ResponseWriter, r *http.Request) {
return
}
dispatcher := cqrs.Must(ctn)
db := orm.Must(ctn).DB()
repo := model.NewUserRepository(db)
cmd := &command.CreateUserCommandRequest{
Email: claims.Email,
Connected: true,
}
if _, err := dispatcher.Exec(ctx, cmd); err != nil {
panic(errors.WithStack(err))
if _, err := repo.CreateOrConnectUser(ctx, claims.Email); err != nil {
panic(errors.Wrap(err, "could not upsert user"))
}
if err := session.SaveUserEmail(w, r, claims.Email); err != nil {

View File

@ -0,0 +1,15 @@
server {
listen 80 default_server;
server_name daddy.local;
root /usr/share/daddy/client/public;
index index.html;
location / {
try_files $uri /index.html =404;
}
location /api/v1/graphql {
proxy_pass http://127.0.0.1:8080/api/v1/graphql;
}
}

View File

@ -0,0 +1 @@
GO_ENV=prod

119
misc/script/release Executable file
View File

@ -0,0 +1,119 @@
#!/bin/bash
set -eo pipefail
OS_TARGETS=(linux)
ARCH_TARGETS=${ARCH_TARGETS:-amd64 arm 386}
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
PROJECT_DIR="$DIR/../.."
function build {
local name=$1
local srcdir=$2
local os=$3
local arch=$4
local dirname="$name-$os-$arch"
local destdir="$PROJECT_DIR/release/$dirname"
rm -rf "$destdir"
mkdir -p "$destdir"
echo "building $dirname..."
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build \
-ldflags="-s -w -X main.GitCommit=$(current_commit_ref) -X main.ProjectVersion=$(current_version)" \
-gcflags=-trimpath="${PWD}" \
-asmflags=-trimpath="${PWD}" \
-o "$destdir/bin/$name" \
"$srcdir"
if [ ! -z "$(which upx)" ]; then
upx --best "$destdir/bin/$name"
fi
}
function current_commit_ref {
git rev-list -1 HEAD
}
function current_version {
local latest_tag=$(git describe --abbrev=0 2>/dev/null)
echo ${latest_tag:-0.0.0}
}
function copy {
local name=$1
local os=$2
local arch=$3
local src=$4
local dest=$5
local dirname="$name-$os-$arch"
local destdir="$PROJECT_DIR/release/$dirname"
echo "copying '$src' to '$destdir/$dest'..."
mkdir -p "$(dirname $destdir/$dest)"
cp -rfL $src "$destdir/$dest"
}
function dump_default_conf {
# Generate and copy configuration file
local command=$1
local os=$2
local arch=$3
local tmp_conf=$(mktemp)
go run "$PROJECT_DIR/cmd/$command" -dump-config > "$tmp_conf"
copy "$command" $os $arch "$tmp_conf" "$command.conf"
rm -f "$tmp_conf"
}
function compress {
local name=$1
local os=$2
local arch=$3
local dirname="$name-$os-$arch"
local destdir="$PROJECT_DIR/release/$dirname"
echo "compressing $dirname..."
tar -czf "$destdir.tar.gz" -C "$destdir/../" "$dirname"
}
function release_server {
local os=$1
local arch=$2
build 'server' "$PROJECT_DIR/cmd/server" $os $arch
dump_default_conf 'server' $os $arch
copy 'server' $os $arch "$PROJECT_DIR/README.md" "README.md"
copy 'server' $os $arch "$PROJECT_DIR/client/dist" "public"
compress 'server' $os $arch
}
function main {
make client-dist
for os in ${OS_TARGETS[@]}; do
for arch in ${ARCH_TARGETS[@]}; do
release_server $os $arch
done
done
}
main

View File

@ -1,4 +1,4 @@
internal/graph/schema.graphqls
internal/graph/*.graphql
internal/gqlgen.yml {
prep: make generate
}
@ -8,7 +8,7 @@ internal/gqlgen.yml {
data/config.yml
.env
modd.conf {
prep: make build-server
prep: make build
prep: [ -e data/config.yml ] || ( mkdir -p data && bin/server -dump-config > data/config.yml )
prep: [ -e .env ] || ( cp .env.dist .env )
prep: make migrate-latest