feature: add web & migrate commands
This commit is contained in:
parent
1e9813601e
commit
90515f6aa4
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
node_modules
|
||||
uploads/*
|
||||
!uploads/.keep
|
||||
|
||||
internal/frontend/frontend_generated.go
|
||||
citadel
|
||||
|
@ -1,62 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jordanknott/project-citadel/api/internal/commands"
|
||||
_ "github.com/lib/pq"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/jordanknott/project-citadel/api/internal/route"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
Host string
|
||||
Name string
|
||||
User string
|
||||
Password string
|
||||
}
|
||||
type AppConfig struct {
|
||||
Database Database
|
||||
}
|
||||
|
||||
func main() {
|
||||
dat, err := ioutil.ReadFile("conf/app.toml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var appConfig AppConfig
|
||||
_, err = toml.Decode(string(dat), &appConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
Formatter := new(log.TextFormatter)
|
||||
Formatter.TimestampFormat = "02-01-2006 15:04:05"
|
||||
Formatter.FullTimestamp = true
|
||||
log.SetFormatter(Formatter)
|
||||
log.SetLevel(log.InfoLevel)
|
||||
connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable",
|
||||
appConfig.Database.User,
|
||||
appConfig.Database.Password,
|
||||
appConfig.Database.Host,
|
||||
appConfig.Database.Name,
|
||||
)
|
||||
db, err := sqlx.Connect("postgres", connection)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxIdleConns(25)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
|
||||
defer db.Close()
|
||||
fmt.Println("starting graphql server on http://localhost:3333")
|
||||
fmt.Println("starting graphql playground on http://localhost:3333/__graphql")
|
||||
r, _ := route.NewRouter(db)
|
||||
http.ListenAndServe(":3333", r)
|
||||
commands.Execute()
|
||||
}
|
||||
|
@ -1,5 +1,24 @@
|
||||
[general]
|
||||
host = '0.0.0.0:3333'
|
||||
|
||||
[email_notifications]
|
||||
enabled = true
|
||||
display_name = "No Reply"
|
||||
from_address = "example.com"
|
||||
|
||||
[storage]
|
||||
storage_system = 'local_storage'
|
||||
upload_dir_path = 'uploads'
|
||||
|
||||
[database]
|
||||
host = '0.0.0.0'
|
||||
name = 'citadel'
|
||||
name = 'citadel_test'
|
||||
user = 'postgres'
|
||||
password = 'test'
|
||||
|
||||
[smtp]
|
||||
username = 'admin@example.com'
|
||||
password = 'example'
|
||||
server = 'mail.example.com'
|
||||
port = 465
|
||||
connection_security = 'STARTTLS'
|
||||
|
@ -70,6 +70,7 @@
|
||||
"styled-components": "^5.0.1",
|
||||
"typescript": "~3.7.2"
|
||||
},
|
||||
"proxy": "http://localhost:3333",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
|
@ -2,19 +2,19 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
|
@ -3,11 +3,11 @@ import Admin from 'shared/components/Admin';
|
||||
import Select from 'shared/components/Select';
|
||||
import GlobalTopNavbar from 'App/TopNavbar';
|
||||
import {
|
||||
useUsersQuery,
|
||||
useDeleteUserAccountMutation,
|
||||
useCreateUserAccountMutation,
|
||||
UsersDocument,
|
||||
UsersQuery,
|
||||
useUsersQuery,
|
||||
useDeleteUserAccountMutation,
|
||||
useCreateUserAccountMutation,
|
||||
UsersDocument,
|
||||
UsersQuery,
|
||||
} from 'shared/generated/graphql';
|
||||
import Input from 'shared/components/Input';
|
||||
import styled from 'styled-components';
|
||||
@ -33,25 +33,25 @@ const DeleteUserButton = styled(Button)`
|
||||
`;
|
||||
|
||||
type DeleteUserPopupProps = {
|
||||
onDeleteUser: () => void;
|
||||
onDeleteUser: () => void;
|
||||
};
|
||||
const DeleteUserPopup: React.FC<DeleteUserPopupProps> = ({ onDeleteUser }) => {
|
||||
return (
|
||||
<DeleteUserWrapper>
|
||||
<DeleteUserDescription>Deleting this user will remove all user related data.</DeleteUserDescription>
|
||||
<DeleteUserButton onClick={() => onDeleteUser()} color="danger">
|
||||
Delete user
|
||||
return (
|
||||
<DeleteUserWrapper>
|
||||
<DeleteUserDescription>Deleting this user will remove all user related data.</DeleteUserDescription>
|
||||
<DeleteUserButton onClick={() => onDeleteUser()} color="danger">
|
||||
Delete user
|
||||
</DeleteUserButton>
|
||||
</DeleteUserWrapper>
|
||||
);
|
||||
</DeleteUserWrapper>
|
||||
);
|
||||
};
|
||||
type CreateUserData = {
|
||||
email: string;
|
||||
username: string;
|
||||
fullName: string;
|
||||
initials: string;
|
||||
password: string;
|
||||
roleCode: string;
|
||||
email: string;
|
||||
username: string;
|
||||
fullName: string;
|
||||
initials: string;
|
||||
password: string;
|
||||
roleCode: string;
|
||||
};
|
||||
const CreateUserForm = styled.form`
|
||||
display: flex;
|
||||
@ -73,162 +73,167 @@ const InputError = styled.span`
|
||||
`;
|
||||
|
||||
type AddUserPopupProps = {
|
||||
onAddUser: (user: CreateUserData) => void;
|
||||
onAddUser: (user: CreateUserData) => void;
|
||||
};
|
||||
|
||||
const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
|
||||
const { register, handleSubmit, errors, setValue } = useForm<CreateUserData>();
|
||||
const [role, setRole] = useState<string | null>(null);
|
||||
register({ name: 'roleCode' }, { required: true });
|
||||
const { register, handleSubmit, errors, setValue } = useForm<CreateUserData>();
|
||||
const [role, setRole] = useState<string | null>(null);
|
||||
register({ name: 'roleCode' }, { required: true });
|
||||
|
||||
const createUser = (data: CreateUserData) => {
|
||||
onAddUser(data);
|
||||
};
|
||||
return (
|
||||
<CreateUserForm onSubmit={handleSubmit(createUser)}>
|
||||
<AddUserInput
|
||||
floatingLabel
|
||||
width="100%"
|
||||
label="Full Name"
|
||||
id="fullName"
|
||||
name="fullName"
|
||||
variant="alternate"
|
||||
ref={register({ required: 'Full name is required' })}
|
||||
/>
|
||||
{errors.fullName && <InputError>{errors.fullName.message}</InputError>}
|
||||
<AddUserInput
|
||||
floatingLabel
|
||||
width="100%"
|
||||
label="Email"
|
||||
id="email"
|
||||
name="email"
|
||||
variant="alternate"
|
||||
ref={register({ required: 'Email is required' })}
|
||||
/>
|
||||
<Select
|
||||
label="Role"
|
||||
value={role}
|
||||
options={[
|
||||
{ label: 'Admin', value: 'admin' },
|
||||
{ label: 'Member', value: 'member' },
|
||||
]}
|
||||
onChange={newRole => {
|
||||
setRole(newRole);
|
||||
setValue('roleCode', newRole.value);
|
||||
}}
|
||||
/>
|
||||
{errors.email && <InputError>{errors.email.message}</InputError>}
|
||||
<AddUserInput
|
||||
floatingLabel
|
||||
width="100%"
|
||||
label="Username"
|
||||
id="username"
|
||||
name="username"
|
||||
variant="alternate"
|
||||
ref={register({ required: 'Username is required' })}
|
||||
/>
|
||||
{errors.username && <InputError>{errors.username.message}</InputError>}
|
||||
<AddUserInput
|
||||
floatingLabel
|
||||
width="100%"
|
||||
label="Initials"
|
||||
id="initials"
|
||||
name="initials"
|
||||
variant="alternate"
|
||||
ref={register({ required: 'Initials is required' })}
|
||||
/>
|
||||
{errors.initials && <InputError>{errors.initials.message}</InputError>}
|
||||
<AddUserInput
|
||||
floatingLabel
|
||||
width="100%"
|
||||
label="Password"
|
||||
id="password"
|
||||
name="password"
|
||||
variant="alternate"
|
||||
ref={register({ required: 'Password is required' })}
|
||||
/>
|
||||
{errors.password && <InputError>{errors.password.message}</InputError>}
|
||||
<CreateUserButton type="submit">Create</CreateUserButton>
|
||||
</CreateUserForm>
|
||||
);
|
||||
const createUser = (data: CreateUserData) => {
|
||||
onAddUser(data);
|
||||
};
|
||||
return (
|
||||
<CreateUserForm onSubmit={handleSubmit(createUser)}>
|
||||
<AddUserInput
|
||||
floatingLabel
|
||||
width="100%"
|
||||
label="Full Name"
|
||||
id="fullName"
|
||||
name="fullName"
|
||||
variant="alternate"
|
||||
ref={register({ required: 'Full name is required' })}
|
||||
/>
|
||||
{errors.fullName && <InputError>{errors.fullName.message}</InputError>}
|
||||
<AddUserInput
|
||||
floatingLabel
|
||||
width="100%"
|
||||
label="Email"
|
||||
id="email"
|
||||
name="email"
|
||||
variant="alternate"
|
||||
ref={register({ required: 'Email is required' })}
|
||||
/>
|
||||
<Select
|
||||
label="Role"
|
||||
value={role}
|
||||
options={[
|
||||
{ label: 'Admin', value: 'admin' },
|
||||
{ label: 'Member', value: 'member' },
|
||||
]}
|
||||
onChange={newRole => {
|
||||
setRole(newRole);
|
||||
setValue('roleCode', newRole.value);
|
||||
}}
|
||||
/>
|
||||
{errors.email && <InputError>{errors.email.message}</InputError>}
|
||||
<AddUserInput
|
||||
floatingLabel
|
||||
width="100%"
|
||||
label="Username"
|
||||
id="username"
|
||||
name="username"
|
||||
variant="alternate"
|
||||
ref={register({ required: 'Username is required' })}
|
||||
/>
|
||||
{errors.username && <InputError>{errors.username.message}</InputError>}
|
||||
<AddUserInput
|
||||
floatingLabel
|
||||
width="100%"
|
||||
label="Initials"
|
||||
id="initials"
|
||||
name="initials"
|
||||
variant="alternate"
|
||||
ref={register({ required: 'Initials is required' })}
|
||||
/>
|
||||
{errors.initials && <InputError>{errors.initials.message}</InputError>}
|
||||
<AddUserInput
|
||||
floatingLabel
|
||||
width="100%"
|
||||
label="Password"
|
||||
id="password"
|
||||
name="password"
|
||||
variant="alternate"
|
||||
ref={register({ required: 'Password is required' })}
|
||||
/>
|
||||
{errors.password && <InputError>{errors.password.message}</InputError>}
|
||||
<CreateUserButton type="submit">Create</CreateUserButton>
|
||||
</CreateUserForm>
|
||||
);
|
||||
};
|
||||
|
||||
const AdminRoute = () => {
|
||||
useEffect(() => {
|
||||
document.title = 'Citadel | Admin';
|
||||
}, []);
|
||||
const { loading, data } = useUsersQuery();
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const [deleteUser] = useDeleteUserAccountMutation({
|
||||
update: (client, response) => {
|
||||
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
|
||||
produce(cache, draftCache => {
|
||||
draftCache.users = cache.users.filter(u => u.id !== response.data.deleteUserAccount.userAccount.id);
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
const [createUser] = useCreateUserAccountMutation({
|
||||
update: (client, createData) => {
|
||||
const cacheData: any = client.readQuery({
|
||||
query: UsersDocument,
|
||||
});
|
||||
console.log(cacheData);
|
||||
console.log(createData);
|
||||
const newData = produce(cacheData, (draftState: any) => {
|
||||
draftState.users = [...draftState.users, { ...createData.data.createUserAccount }];
|
||||
});
|
||||
|
||||
client.writeQuery({
|
||||
query: UsersDocument,
|
||||
data: {
|
||||
...newData,
|
||||
useEffect(() => {
|
||||
document.title = 'Citadel | Admin';
|
||||
}, []);
|
||||
const { loading, data } = useUsersQuery();
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const [deleteUser] = useDeleteUserAccountMutation({
|
||||
update: (client, response) => {
|
||||
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
|
||||
produce(cache, draftCache => {
|
||||
draftCache.users = cache.users.filter(u => u.id !== response.data.deleteUserAccount.userAccount.id);
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
if (loading) {
|
||||
return <GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />;
|
||||
}
|
||||
if (data) {
|
||||
return (
|
||||
<>
|
||||
<GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />
|
||||
<Admin
|
||||
initialTab={1}
|
||||
users={data.users}
|
||||
onInviteUser={() => {}}
|
||||
onDeleteUser={($target, userID) => {
|
||||
showPopup(
|
||||
$target,
|
||||
<Popup tab={0} title="Delete user?" onClose={() => hidePopup()}>
|
||||
<DeleteUserPopup
|
||||
onDeleteUser={() => {
|
||||
deleteUser({ variables: { userID } });
|
||||
hidePopup();
|
||||
}}
|
||||
});
|
||||
const [createUser] = useCreateUserAccountMutation({
|
||||
update: (client, createData) => {
|
||||
const cacheData: any = client.readQuery({
|
||||
query: UsersDocument,
|
||||
});
|
||||
console.log(cacheData);
|
||||
console.log(createData);
|
||||
const newData = produce(cacheData, (draftState: any) => {
|
||||
draftState.users = [...draftState.users, { ...createData.data.createUserAccount }];
|
||||
});
|
||||
|
||||
client.writeQuery({
|
||||
query: UsersDocument,
|
||||
data: {
|
||||
...newData,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
if (loading) {
|
||||
return <GlobalTopNavbar projectID={null} onSaveProjectName={() => { }} name={null} />;
|
||||
}
|
||||
if (data) {
|
||||
return (
|
||||
<>
|
||||
<GlobalTopNavbar projectID={null} onSaveProjectName={() => { }} name={null} />
|
||||
<Admin
|
||||
initialTab={1}
|
||||
users={data.users}
|
||||
onInviteUser={() => { }}
|
||||
onUpdateUserPassword={(user, password) => {
|
||||
console.log(user)
|
||||
console.log(password)
|
||||
hidePopup()
|
||||
}}
|
||||
onDeleteUser={($target, userID) => {
|
||||
showPopup(
|
||||
$target,
|
||||
<Popup tab={0} title="Delete user?" onClose={() => hidePopup()}>
|
||||
<DeleteUserPopup
|
||||
onDeleteUser={() => {
|
||||
deleteUser({ variables: { userID } });
|
||||
hidePopup();
|
||||
}}
|
||||
/>
|
||||
</Popup>,
|
||||
);
|
||||
}}
|
||||
onAddUser={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<Popup tab={0} title="Add member" onClose={() => hidePopup()}>
|
||||
<AddUserPopup
|
||||
onAddUser={user => {
|
||||
createUser({ variables: { ...user } });
|
||||
hidePopup();
|
||||
}}
|
||||
/>
|
||||
</Popup>,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Popup>,
|
||||
);
|
||||
}}
|
||||
onAddUser={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<Popup tab={0} title="Add member" onClose={() => hidePopup()}>
|
||||
<AddUserPopup
|
||||
onAddUser={user => {
|
||||
createUser({ variables: { ...user } });
|
||||
hidePopup();
|
||||
}}
|
||||
/>
|
||||
</Popup>,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <span>error</span>;
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <span>error</span>;
|
||||
};
|
||||
|
||||
export default AdminRoute;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import TopNavbar, { MenuItem } from 'shared/components/TopNavbar';
|
||||
import React, {useState, useContext, useEffect} from 'react';
|
||||
import TopNavbar, {MenuItem} from 'shared/components/TopNavbar';
|
||||
import styled from 'styled-components/macro';
|
||||
import DropdownMenu, { ProfileMenu } from 'shared/components/DropdownMenu';
|
||||
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
||||
import { useHistory } from 'react-router';
|
||||
import DropdownMenu, {ProfileMenu} from 'shared/components/DropdownMenu';
|
||||
import ProjectSettings, {DeleteConfirm, DELETE_INFO} from 'shared/components/ProjectSettings';
|
||||
import {useHistory} from 'react-router';
|
||||
import UserIDContext from 'App/context';
|
||||
import {
|
||||
RoleCode,
|
||||
@ -12,10 +12,10 @@ import {
|
||||
useGetProjectsQuery,
|
||||
GetProjectsDocument,
|
||||
} from 'shared/generated/graphql';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import { History } from 'history';
|
||||
import {usePopup, Popup} from 'shared/components/PopupMenu';
|
||||
import {History} from 'history';
|
||||
import produce from 'immer';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
const TeamContainer = styled.div`
|
||||
display: flex;
|
||||
@ -45,7 +45,7 @@ const TeamProjectLink = styled(Link)`
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
const TeamProjectBackground = styled.div<{ color: string }>`
|
||||
const TeamProjectBackground = styled.div<{color: string}>`
|
||||
background-image: url(null);
|
||||
background-color: ${props => props.color};
|
||||
|
||||
@ -68,7 +68,7 @@ const TeamProjectBackground = styled.div<{ color: string }>`
|
||||
}
|
||||
`;
|
||||
|
||||
const TeamProjectAvatar = styled.div<{ color: string }>`
|
||||
const TeamProjectAvatar = styled.div<{color: string}>`
|
||||
background-image: url(null);
|
||||
background-color: ${props => props.color};
|
||||
|
||||
@ -122,12 +122,12 @@ const TeamProjectContainer = styled.div`
|
||||
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
|
||||
|
||||
const ProjectFinder = () => {
|
||||
const { loading, data } = useGetProjectsQuery();
|
||||
const {loading, data} = useGetProjectsQuery();
|
||||
if (loading) {
|
||||
return <span>loading</span>;
|
||||
}
|
||||
if (data) {
|
||||
const { projects, teams, organizations } = data;
|
||||
const {projects, teams, organizations} = data;
|
||||
const projectTeams = teams.map(team => {
|
||||
return {
|
||||
id: team.id,
|
||||
@ -166,8 +166,8 @@ type ProjectPopupProps = {
|
||||
projectID: string;
|
||||
};
|
||||
|
||||
export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, projectID }) => {
|
||||
const { hidePopup, setTab } = usePopup();
|
||||
export const ProjectPopup: React.FC<ProjectPopupProps> = ({history, name, projectID}) => {
|
||||
const {hidePopup, setTab} = usePopup();
|
||||
const [deleteProject] = useDeleteProjectMutation({
|
||||
update: (client, deleteData) => {
|
||||
const cacheData: any = client.readQuery({
|
||||
@ -206,7 +206,7 @@ export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, proje
|
||||
deletedItems={DELETE_INFO.DELETE_PROJECTS.deletedItems}
|
||||
onConfirmDelete={() => {
|
||||
if (projectID) {
|
||||
deleteProject({ variables: { projectID } });
|
||||
deleteProject({variables: {projectID}});
|
||||
hidePopup();
|
||||
history.push('/projects');
|
||||
}
|
||||
@ -249,16 +249,16 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
nameOnly,
|
||||
}) => {
|
||||
console.log(popupContent);
|
||||
const { loading, data } = useMeQuery();
|
||||
const { showPopup, hidePopup, setTab } = usePopup();
|
||||
const {loading, data} = useMeQuery();
|
||||
const {showPopup, hidePopup, setTab} = usePopup();
|
||||
const history = useHistory();
|
||||
const { userID, setUserID } = useContext(UserIDContext);
|
||||
const {userID, setUserID} = useContext(UserIDContext);
|
||||
const onLogout = () => {
|
||||
fetch('http://localhost:3333/auth/logout', {
|
||||
fetch('/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
}).then(async x => {
|
||||
const { status } = x;
|
||||
const {status} = x;
|
||||
if (status === 200) {
|
||||
history.replace('/login');
|
||||
setUserID(null);
|
||||
|
@ -1,16 +1,16 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { setAccessToken } from 'shared/utils/accessToken';
|
||||
import styled, { ThemeProvider } from 'styled-components';
|
||||
import {createBrowserHistory} from 'history';
|
||||
import {setAccessToken} from 'shared/utils/accessToken';
|
||||
import styled, {ThemeProvider} from 'styled-components';
|
||||
import NormalizeStyles from './NormalizeStyles';
|
||||
import BaseStyles from './BaseStyles';
|
||||
import { theme } from './ThemeStyles';
|
||||
import {theme} from './ThemeStyles';
|
||||
import Routes from './Routes';
|
||||
import { UserIDContext } from './context';
|
||||
import {UserIDContext} from './context';
|
||||
import Navbar from './Navbar';
|
||||
import { Router } from 'react-router';
|
||||
import { PopupProvider } from 'shared/components/PopupMenu';
|
||||
import {Router} from 'react-router';
|
||||
import {PopupProvider} from 'shared/components/PopupMenu';
|
||||
|
||||
const history = createBrowserHistory();
|
||||
|
||||
@ -19,16 +19,16 @@ const App = () => {
|
||||
const [userID, setUserID] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('http://localhost:3333/auth/refresh_token', {
|
||||
fetch('/auth/refresh_token', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
}).then(async x => {
|
||||
const { status } = x;
|
||||
const {status} = x;
|
||||
if (status === 400) {
|
||||
history.replace('/login');
|
||||
} else {
|
||||
const response: RefreshTokenResponse = await x.json();
|
||||
const { accessToken } = response;
|
||||
const {accessToken} = response;
|
||||
const claims: JWTToken = jwtDecode(accessToken);
|
||||
setUserID(claims.userId);
|
||||
setAccessToken(accessToken);
|
||||
@ -39,7 +39,7 @@ const App = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserIDContext.Provider value={{ userID, setUserID }}>
|
||||
<UserIDContext.Provider value={{userID, setUserID}}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<NormalizeStyles />
|
||||
<BaseStyles />
|
||||
@ -48,10 +48,10 @@ const App = () => {
|
||||
{loading ? (
|
||||
<div>loading</div>
|
||||
) : (
|
||||
<>
|
||||
<Routes history={history} />
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<Routes history={history} />
|
||||
</>
|
||||
)}
|
||||
</PopupProvider>
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
|
@ -1,24 +1,24 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useHistory } from 'react-router';
|
||||
import React, {useState, useEffect, useContext} from 'react';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {useHistory} from 'react-router';
|
||||
|
||||
import { setAccessToken } from 'shared/utils/accessToken';
|
||||
import {setAccessToken} from 'shared/utils/accessToken';
|
||||
|
||||
import Login from 'shared/components/Login';
|
||||
import { Container, LoginWrapper } from './Styles';
|
||||
import {Container, LoginWrapper} from './Styles';
|
||||
import UserIDContext from 'App/context';
|
||||
import JwtDecode from 'jwt-decode';
|
||||
|
||||
const Auth = () => {
|
||||
const [invalidLoginAttempt, setInvalidLoginAttempt] = useState(0);
|
||||
const history = useHistory();
|
||||
const { setUserID } = useContext(UserIDContext);
|
||||
const {setUserID} = useContext(UserIDContext);
|
||||
const login = (
|
||||
data: LoginFormData,
|
||||
setComplete: (val: boolean) => void,
|
||||
setError: (field: string, eType: string, message: string) => void,
|
||||
) => {
|
||||
fetch('http://localhost:3333/auth/login', {
|
||||
fetch('/auth/login', {
|
||||
credentials: 'include',
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
@ -33,7 +33,7 @@ const Auth = () => {
|
||||
setComplete(true);
|
||||
} else {
|
||||
const response = await x.json();
|
||||
const { accessToken } = response;
|
||||
const {accessToken} = response;
|
||||
const claims: JWTToken = JwtDecode(accessToken);
|
||||
setUserID(claims.userId);
|
||||
setComplete(true);
|
||||
@ -45,11 +45,11 @@ const Auth = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch('http://localhost:3333/auth/refresh_token', {
|
||||
fetch('/auth/refresh_token', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
}).then(async x => {
|
||||
const { status } = x;
|
||||
const {status} = x;
|
||||
if (status === 200) {
|
||||
history.replace('/projects');
|
||||
}
|
||||
|
@ -2,13 +2,13 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import axios from 'axios';
|
||||
import createAuthRefreshInterceptor from 'axios-auth-refresh';
|
||||
import { ApolloProvider } from '@apollo/react-hooks';
|
||||
import { ApolloClient } from 'apollo-client';
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||
import { HttpLink } from 'apollo-link-http';
|
||||
import { onError } from 'apollo-link-error';
|
||||
import { ApolloLink, Observable, fromPromise } from 'apollo-link';
|
||||
import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken';
|
||||
import {ApolloProvider} from '@apollo/react-hooks';
|
||||
import {ApolloClient} from 'apollo-client';
|
||||
import {InMemoryCache} from 'apollo-cache-inmemory';
|
||||
import {HttpLink} from 'apollo-link-http';
|
||||
import {onError} from 'apollo-link-error';
|
||||
import {ApolloLink, Observable, fromPromise} from 'apollo-link';
|
||||
import {getAccessToken, getNewToken, setAccessToken} from 'shared/utils/accessToken';
|
||||
import App from './App';
|
||||
|
||||
// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8
|
||||
@ -18,7 +18,7 @@ let isRefreshing = false;
|
||||
let pendingRequests: any = [];
|
||||
|
||||
const refreshAuthLogic = (failedRequest: any) =>
|
||||
axios.post('http://localhost:3333/auth/refresh_token', {}, { withCredentials: true }).then(tokenRefreshResponse => {
|
||||
axios.post('/auth/refresh_token', {}, {withCredentials: true}).then(tokenRefreshResponse => {
|
||||
setAccessToken(tokenRefreshResponse.data.accessToken);
|
||||
failedRequest.response.config.headers.Authorization = `Bearer ${tokenRefreshResponse.data.accessToken}`;
|
||||
return Promise.resolve();
|
||||
@ -43,7 +43,7 @@ const setRefreshing = (newVal: boolean) => {
|
||||
isRefreshing = newVal;
|
||||
};
|
||||
|
||||
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
|
||||
const errorLink = onError(({graphQLErrors, networkError, operation, forward}) => {
|
||||
if (graphQLErrors) {
|
||||
for (const err of graphQLErrors) {
|
||||
if (err.extensions && err.extensions.code) {
|
||||
@ -118,9 +118,9 @@ const requestLink = new ApolloLink(
|
||||
|
||||
const client = new ApolloClient({
|
||||
link: ApolloLink.from([
|
||||
onError(({ graphQLErrors, networkError }) => {
|
||||
onError(({graphQLErrors, networkError}) => {
|
||||
if (graphQLErrors) {
|
||||
graphQLErrors.forEach(({ message, locations, path }) =>
|
||||
graphQLErrors.forEach(({message, locations, path}) =>
|
||||
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`),
|
||||
);
|
||||
}
|
||||
@ -131,7 +131,7 @@ const client = new ApolloClient({
|
||||
errorLink,
|
||||
requestLink,
|
||||
new HttpLink({
|
||||
uri: 'http://localhost:3333/graphql',
|
||||
uri: '/graphql',
|
||||
credentials: 'same-origin',
|
||||
}),
|
||||
]),
|
||||
|
@ -1,18 +1,18 @@
|
||||
import React, { useRef } from 'react';
|
||||
import React, {useRef} from 'react';
|
||||
import Admin from '.';
|
||||
import { theme } from 'App/ThemeStyles';
|
||||
import {theme} from 'App/ThemeStyles';
|
||||
import NormalizeStyles from 'App/NormalizeStyles';
|
||||
import BaseStyles from 'App/BaseStyles';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import {ThemeProvider} from 'styled-components';
|
||||
import {action} from '@storybook/addon-actions';
|
||||
|
||||
export default {
|
||||
component: Admin,
|
||||
title: 'Admin',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'gray', value: '#f8f8f8', default: true },
|
||||
{ name: 'white', value: '#ffffff' },
|
||||
{name: 'gray', value: '#f8f8f8', default: true},
|
||||
{name: 'white', value: '#ffffff'},
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -26,13 +26,14 @@ export const Default = () => {
|
||||
<Admin
|
||||
onInviteUser={action('invite user')}
|
||||
initialTab={1}
|
||||
onUpdateUserPassword={action('update user password')}
|
||||
onDeleteUser={action('delete user')}
|
||||
users={[
|
||||
{
|
||||
id: '1',
|
||||
username: 'jordanthedev',
|
||||
email: 'jordan@jordanthedev.com',
|
||||
role: { code: 'admin', name: 'Admin' },
|
||||
role: {code: 'admin', name: 'Admin'},
|
||||
fullName: 'Jordan Knott',
|
||||
profileIcon: {
|
||||
bgColor: '#fff',
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React, {useState, useRef} from 'react';
|
||||
import {UserPlus, Checkmark} from 'shared/icons';
|
||||
import styled, {css} from 'styled-components';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { UserPlus, Checkmark } from 'shared/icons';
|
||||
import styled, { css } from 'styled-components';
|
||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||
import Select from 'shared/components/Select';
|
||||
import {User, Plus, Lock, Pencil, Trash} from 'shared/icons';
|
||||
import {usePopup, Popup} from 'shared/components/PopupMenu';
|
||||
import {RoleCode, useUpdateUserRoleMutation} from 'shared/generated/graphql';
|
||||
import {AgGridReact} from 'ag-grid-react';
|
||||
import { User, Plus, Lock, Pencil, Trash } from 'shared/icons';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import { RoleCode, useUpdateUserRoleMutation } from 'shared/generated/graphql';
|
||||
import { AgGridReact } from 'ag-grid-react';
|
||||
import Input from 'shared/components/Input';
|
||||
import Member from 'shared/components/Member';
|
||||
|
||||
@ -19,20 +19,20 @@ export const RoleCheckmark = styled(Checkmark)`
|
||||
`;
|
||||
|
||||
const permissions = [
|
||||
{
|
||||
code: 'owner',
|
||||
name: 'Owner',
|
||||
description:
|
||||
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team. Can delete the team and all team projects.',
|
||||
},
|
||||
{
|
||||
code: 'admin',
|
||||
name: 'Admin',
|
||||
description:
|
||||
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team.',
|
||||
},
|
||||
{
|
||||
code: 'owner',
|
||||
name: 'Owner',
|
||||
description:
|
||||
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team. Can delete the team and all team projects.',
|
||||
},
|
||||
{
|
||||
code: 'admin',
|
||||
name: 'Admin',
|
||||
description:
|
||||
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team.',
|
||||
},
|
||||
|
||||
{code: 'member', name: 'Member', description: 'Can view, create, and edit team projects, but not change settings.'},
|
||||
{ code: 'member', name: 'Member', description: 'Can view, create, and edit team projects, but not change settings.' },
|
||||
];
|
||||
|
||||
export const RoleName = styled.div`
|
||||
@ -50,7 +50,7 @@ export const MiniProfileActions = styled.ul`
|
||||
|
||||
export const MiniProfileActionWrapper = styled.li``;
|
||||
|
||||
export const MiniProfileActionItem = styled.span<{disabled?: boolean}>`
|
||||
export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
|
||||
color: #c2c6dc;
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
@ -59,13 +59,13 @@ export const MiniProfileActionItem = styled.span<{disabled?: boolean}>`
|
||||
text-decoration: none;
|
||||
|
||||
${props =>
|
||||
props.disabled
|
||||
? css`
|
||||
props.disabled
|
||||
? css`
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
color: rgba(${props.theme.colors.text.primary}, 0.4);
|
||||
`
|
||||
: css`
|
||||
: css`
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: rgb(115, 103, 240);
|
||||
@ -104,142 +104,149 @@ export const RemoveMemberButton = styled(Button)`
|
||||
width: 100%;
|
||||
`;
|
||||
type TeamRoleManagerPopupProps = {
|
||||
user: TaskUser;
|
||||
warning?: string | null;
|
||||
canChangeRole: boolean;
|
||||
onChangeRole: (roleCode: RoleCode) => void;
|
||||
onRemoveFromTeam?: () => void;
|
||||
user: TaskUser;
|
||||
warning?: string | null;
|
||||
canChangeRole: boolean;
|
||||
onChangeRole: (roleCode: RoleCode) => void;
|
||||
updateUserPassword?: (user: TaskUser, password: string) => void;
|
||||
onRemoveFromTeam?: () => void;
|
||||
};
|
||||
|
||||
const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
||||
warning,
|
||||
user,
|
||||
canChangeRole,
|
||||
onRemoveFromTeam,
|
||||
onChangeRole,
|
||||
warning,
|
||||
user,
|
||||
canChangeRole,
|
||||
onRemoveFromTeam,
|
||||
updateUserPassword,
|
||||
onChangeRole,
|
||||
}) => {
|
||||
const {hidePopup, setTab} = usePopup();
|
||||
return (
|
||||
<>
|
||||
<Popup title={null} tab={0}>
|
||||
<MiniProfileActions>
|
||||
<MiniProfileActionWrapper>
|
||||
{user.role && (
|
||||
<MiniProfileActionItem
|
||||
onClick={() => {
|
||||
setTab(1);
|
||||
}}
|
||||
>
|
||||
Change permissions...
|
||||
const { hidePopup, setTab } = usePopup();
|
||||
const [userPass, setUserPass] = useState({ pass: "", passConfirm: "" });
|
||||
return (
|
||||
<>
|
||||
<Popup title={null} tab={0}>
|
||||
<MiniProfileActions>
|
||||
<MiniProfileActionWrapper>
|
||||
{user.role && (
|
||||
<MiniProfileActionItem
|
||||
onClick={() => {
|
||||
setTab(1);
|
||||
}}
|
||||
>
|
||||
Change permissions...
|
||||
<CurrentPermission>{`(${user.role.name})`}</CurrentPermission>
|
||||
</MiniProfileActionItem>
|
||||
)}
|
||||
<MiniProfileActionItem onClick={() => {
|
||||
setTab(3)
|
||||
}}>Reset password...</MiniProfileActionItem>
|
||||
<MiniProfileActionItem onClick={() =>setTab(5)}>Remove from organzation...</MiniProfileActionItem>
|
||||
</MiniProfileActionWrapper>
|
||||
</MiniProfileActions>
|
||||
{warning && (
|
||||
<>
|
||||
<Separator />
|
||||
<WarningText>{warning}</WarningText>
|
||||
</>
|
||||
)}
|
||||
</Popup>
|
||||
<Popup title="Change Permissions" onClose={() => hidePopup()} tab={1}>
|
||||
<MiniProfileActions>
|
||||
<MiniProfileActionWrapper>
|
||||
{permissions
|
||||
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
|
||||
.map(perm => (
|
||||
<MiniProfileActionItem
|
||||
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
|
||||
key={perm.code}
|
||||
onClick={() => {
|
||||
if (onChangeRole && user.role && perm.code !== user.role.code) {
|
||||
switch (perm.code) {
|
||||
case 'owner':
|
||||
onChangeRole(RoleCode.Owner);
|
||||
break;
|
||||
case 'admin':
|
||||
onChangeRole(RoleCode.Admin);
|
||||
break;
|
||||
case 'member':
|
||||
onChangeRole(RoleCode.Member);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
hidePopup();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RoleName>
|
||||
{perm.name}
|
||||
{user.role && perm.code === user.role.code && <RoleCheckmark width={12} height={12} />}
|
||||
</RoleName>
|
||||
<RoleDescription>{perm.description}</RoleDescription>
|
||||
</MiniProfileActionItem>
|
||||
))}
|
||||
</MiniProfileActionWrapper>
|
||||
{user.role && user.role.code === 'owner' && (
|
||||
<>
|
||||
<Separator />
|
||||
<WarningText>You can't change roles because there must be an owner.</WarningText>
|
||||
</>
|
||||
)}
|
||||
</MiniProfileActions>
|
||||
</Popup>
|
||||
<Popup title="Remove from Team?" onClose={() => hidePopup()} tab={2}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
The member will be removed from all cards on this project. They will receive a notification.
|
||||
</MiniProfileActionItem>
|
||||
)}
|
||||
<MiniProfileActionItem onClick={() => {
|
||||
setTab(3)
|
||||
}}>Reset password...</MiniProfileActionItem>
|
||||
<MiniProfileActionItem onClick={() => setTab(5)}>Remove from organzation...</MiniProfileActionItem>
|
||||
</MiniProfileActionWrapper>
|
||||
</MiniProfileActions>
|
||||
{warning && (
|
||||
<>
|
||||
<Separator />
|
||||
<WarningText>{warning}</WarningText>
|
||||
</>
|
||||
)}
|
||||
</Popup>
|
||||
<Popup title="Change Permissions" onClose={() => hidePopup()} tab={1}>
|
||||
<MiniProfileActions>
|
||||
<MiniProfileActionWrapper>
|
||||
{permissions
|
||||
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
|
||||
.map(perm => (
|
||||
<MiniProfileActionItem
|
||||
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
|
||||
key={perm.code}
|
||||
onClick={() => {
|
||||
if (onChangeRole && user.role && perm.code !== user.role.code) {
|
||||
switch (perm.code) {
|
||||
case 'owner':
|
||||
onChangeRole(RoleCode.Owner);
|
||||
break;
|
||||
case 'admin':
|
||||
onChangeRole(RoleCode.Admin);
|
||||
break;
|
||||
case 'member':
|
||||
onChangeRole(RoleCode.Member);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
hidePopup();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RoleName>
|
||||
{perm.name}
|
||||
{user.role && perm.code === user.role.code && <RoleCheckmark width={12} height={12} />}
|
||||
</RoleName>
|
||||
<RoleDescription>{perm.description}</RoleDescription>
|
||||
</MiniProfileActionItem>
|
||||
))}
|
||||
</MiniProfileActionWrapper>
|
||||
{user.role && user.role.code === 'owner' && (
|
||||
<>
|
||||
<Separator />
|
||||
<WarningText>You can't change roles because there must be an owner.</WarningText>
|
||||
</>
|
||||
)}
|
||||
</MiniProfileActions>
|
||||
</Popup>
|
||||
<Popup title="Remove from Team?" onClose={() => hidePopup()} tab={2}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
The member will be removed from all cards on this project. They will receive a notification.
|
||||
</DeleteDescription>
|
||||
<RemoveMemberButton
|
||||
color="danger"
|
||||
onClick={() => {
|
||||
if (onRemoveFromTeam) {
|
||||
onRemoveFromTeam();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove Member
|
||||
<RemoveMemberButton
|
||||
color="danger"
|
||||
onClick={() => {
|
||||
if (onRemoveFromTeam) {
|
||||
onRemoveFromTeam();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove Member
|
||||
</RemoveMemberButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
<Popup title="Reset password?" onClose={() => hidePopup()} tab={3}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
You can either set the user's new password directly or send the user an email allowing them to reset their own password.
|
||||
</Content>
|
||||
</Popup>
|
||||
<Popup title="Reset password?" onClose={() => hidePopup()} tab={3}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
You can either set the user's new password directly or send the user an email allowing them to reset their own password.
|
||||
</DeleteDescription>
|
||||
<UserPassBar>
|
||||
<UserPassButton onClick={() => setTab(4)} color="warning">Set password...</UserPassButton>
|
||||
<UserPassButton color="warning" variant="outline">Send reset link</UserPassButton>
|
||||
</UserPassBar>
|
||||
</Content>
|
||||
</Popup>
|
||||
<Popup title="Reset password" onClose={() => hidePopup()} tab={4}>
|
||||
<Content>
|
||||
<NewUserPassInput width="100%" variant="alternate" placeholder="New password" />
|
||||
<NewUserPassInput width="100%" variant="alternate" placeholder="New password (confirm)" />
|
||||
<UserPassConfirmButton onClick={() => {}} color="danger">Set password</UserPassConfirmButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
<Popup title="Remove user" onClose={() => hidePopup()} tab={5}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
|
||||
<UserPassBar>
|
||||
<UserPassButton onClick={() => setTab(4)} color="warning">Set password...</UserPassButton>
|
||||
<UserPassButton color="warning" variant="outline">Send reset link</UserPassButton>
|
||||
</UserPassBar>
|
||||
</Content>
|
||||
</Popup>
|
||||
<Popup title="Reset password" onClose={() => hidePopup()} tab={4}>
|
||||
<Content>
|
||||
<NewUserPassInput onChange={e => setUserPass({ pass: e.currentTarget.value, passConfirm: userPass.passConfirm })} value={userPass.pass} width="100%" variant="alternate" placeholder="New password" />
|
||||
<NewUserPassInput onChange={e => setUserPass({ passConfirm: e.currentTarget.value, pass: userPass.pass })} value={userPass.passConfirm} width="100%" variant="alternate" placeholder="New password (confirm)" />
|
||||
<UserPassConfirmButton disabled={userPass.pass === "" || userPass.passConfirm === ""} onClick={() => {
|
||||
if (userPass.pass === userPass.passConfirm && updateUserPassword) {
|
||||
updateUserPassword(user, userPass.pass)
|
||||
}
|
||||
}} color="danger">Set password</UserPassConfirmButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
<Popup title="Remove user" onClose={() => hidePopup()} tab={5}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
|
||||
</DeleteDescription>
|
||||
<DeleteDescription>
|
||||
The user is the owner of 3 projects & 2 teams.
|
||||
<DeleteDescription>
|
||||
The user is the owner of 3 projects & 2 teams.
|
||||
</DeleteDescription>
|
||||
<UserSelect onChange={() => {}} value={null} options={[{label: 'Jordan Knott', value: "jordanknott"}]} />
|
||||
<UserPassConfirmButton onClick={() => {}} color="danger">Set password</UserPassConfirmButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
</>
|
||||
);
|
||||
<UserSelect onChange={() => { }} value={null} options={[{ label: 'Jordan Knott', value: "jordanknott" }]} />
|
||||
<UserPassConfirmButton onClick={() => { }} color="danger">Set password</UserPassConfirmButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const UserSelect = styled(Select)`
|
||||
margin: 8px 0;
|
||||
@ -406,7 +413,7 @@ const LockUserIcon = styled(Lock)``;
|
||||
const DeleteUserIcon = styled(Trash)``;
|
||||
|
||||
type ActionButtonProps = {
|
||||
onClick: ($target: React.RefObject<HTMLElement>) => void;
|
||||
onClick: ($target: React.RefObject<HTMLElement>) => void;
|
||||
};
|
||||
|
||||
const ActionButtonWrapper = styled.div`
|
||||
@ -415,85 +422,85 @@ const ActionButtonWrapper = styled.div`
|
||||
display: inline-flex;
|
||||
`;
|
||||
|
||||
const ActionButton: React.FC<ActionButtonProps> = ({onClick, children}) => {
|
||||
const $wrapper = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<ActionButtonWrapper onClick={() => onClick($wrapper)} ref={$wrapper}>
|
||||
{children}
|
||||
</ActionButtonWrapper>
|
||||
);
|
||||
const ActionButton: React.FC<ActionButtonProps> = ({ onClick, children }) => {
|
||||
const $wrapper = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<ActionButtonWrapper onClick={() => onClick($wrapper)} ref={$wrapper}>
|
||||
{children}
|
||||
</ActionButtonWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const ActionButtons = (params: any) => {
|
||||
return (
|
||||
<>
|
||||
<ActionButton onClick={() => {}}>
|
||||
<EditUserIcon width={16} height={16} />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={$target => params.onDeleteUser($target, params.value)}>
|
||||
<DeleteUserIcon width={16} height={16} />
|
||||
</ActionButton>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ActionButton onClick={() => { }}>
|
||||
<EditUserIcon width={16} height={16} />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={$target => params.onDeleteUser($target, params.value)}>
|
||||
<DeleteUserIcon width={16} height={16} />
|
||||
</ActionButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type ListTableProps = {
|
||||
users: Array<User>;
|
||||
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
|
||||
users: Array<User>;
|
||||
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
|
||||
};
|
||||
|
||||
const ListTable: React.FC<ListTableProps> = ({users, onDeleteUser}) => {
|
||||
const data = {
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
},
|
||||
columnDefs: [
|
||||
{
|
||||
minWidth: 55,
|
||||
width: 55,
|
||||
headerCheckboxSelection: true,
|
||||
checkboxSelection: true,
|
||||
},
|
||||
{minWidth: 210, headerName: 'Username', editable: true, field: 'username'},
|
||||
{minWidth: 225, headerName: 'Email', field: 'email'},
|
||||
{minWidth: 200, headerName: 'Name', editable: true, field: 'fullName'},
|
||||
{minWidth: 200, headerName: 'Role', editable: true, field: 'roleName'},
|
||||
{
|
||||
minWidth: 200,
|
||||
headerName: 'Actions',
|
||||
field: 'id',
|
||||
cellRenderer: 'actionButtons',
|
||||
cellRendererParams: {
|
||||
onDeleteUser: (target: any, userID: any) => {
|
||||
onDeleteUser(target, userID);
|
||||
},
|
||||
const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => {
|
||||
const data = {
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
frameworkComponents: {
|
||||
actionButtons: ActionButtons,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Root>
|
||||
<div className="ag-theme-material" style={{height: '296px', width: '100%'}}>
|
||||
<AgGridReact
|
||||
rowSelection="multiple"
|
||||
defaultColDef={data.defaultColDef}
|
||||
columnDefs={data.columnDefs}
|
||||
rowData={users.map(u => ({...u, roleName: u.role.name}))}
|
||||
frameworkComponents={data.frameworkComponents}
|
||||
onFirstDataRendered={params => {
|
||||
params.api.sizeColumnsToFit();
|
||||
}}
|
||||
onGridSizeChanged={params => {
|
||||
params.api.sizeColumnsToFit();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Root>
|
||||
);
|
||||
columnDefs: [
|
||||
{
|
||||
minWidth: 55,
|
||||
width: 55,
|
||||
headerCheckboxSelection: true,
|
||||
checkboxSelection: true,
|
||||
},
|
||||
{ minWidth: 210, headerName: 'Username', editable: true, field: 'username' },
|
||||
{ minWidth: 225, headerName: 'Email', field: 'email' },
|
||||
{ minWidth: 200, headerName: 'Name', editable: true, field: 'fullName' },
|
||||
{ minWidth: 200, headerName: 'Role', editable: true, field: 'roleName' },
|
||||
{
|
||||
minWidth: 200,
|
||||
headerName: 'Actions',
|
||||
field: 'id',
|
||||
cellRenderer: 'actionButtons',
|
||||
cellRendererParams: {
|
||||
onDeleteUser: (target: any, userID: any) => {
|
||||
onDeleteUser(target, userID);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
frameworkComponents: {
|
||||
actionButtons: ActionButtons,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Root>
|
||||
<div className="ag-theme-material" style={{ height: '296px', width: '100%' }}>
|
||||
<AgGridReact
|
||||
rowSelection="multiple"
|
||||
defaultColDef={data.defaultColDef}
|
||||
columnDefs={data.columnDefs}
|
||||
rowData={users.map(u => ({ ...u, roleName: u.role.name }))}
|
||||
frameworkComponents={data.frameworkComponents}
|
||||
onFirstDataRendered={params => {
|
||||
params.api.sizeColumnsToFit();
|
||||
}}
|
||||
onGridSizeChanged={params => {
|
||||
params.api.sizeColumnsToFit();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Root>
|
||||
);
|
||||
};
|
||||
|
||||
const Wrapper = styled.div`
|
||||
@ -534,7 +541,7 @@ const TabNavItem = styled.li`
|
||||
display: block;
|
||||
position: relative;
|
||||
`;
|
||||
const TabNavItemButton = styled.button<{active: boolean}>`
|
||||
const TabNavItemButton = styled.button<{ active: boolean }>`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -561,7 +568,7 @@ const TabNavItemSpan = styled.span`
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
const TabNavLine = styled.span<{top: number}>`
|
||||
const TabNavLine = styled.span<{ top: number }>`
|
||||
left: auto;
|
||||
right: 0;
|
||||
width: 2px;
|
||||
@ -594,137 +601,141 @@ const TabContent = styled.div`
|
||||
border-radius: 0.5rem;
|
||||
`;
|
||||
|
||||
const items = [{name: 'Members'}, {name: 'Settings'}];
|
||||
const items = [{ name: 'Members' }, { name: 'Settings' }];
|
||||
|
||||
type NavItemProps = {
|
||||
active: boolean;
|
||||
name: string;
|
||||
tab: number;
|
||||
onClick: (tab: number, top: number) => void;
|
||||
active: boolean;
|
||||
name: string;
|
||||
tab: number;
|
||||
onClick: (tab: number, top: number) => void;
|
||||
};
|
||||
const NavItem: React.FC<NavItemProps> = ({active, name, tab, onClick}) => {
|
||||
const $item = useRef<HTMLLIElement>(null);
|
||||
return (
|
||||
<TabNavItem
|
||||
key={name}
|
||||
ref={$item}
|
||||
onClick={() => {
|
||||
if ($item && $item.current) {
|
||||
const pos = $item.current.getBoundingClientRect();
|
||||
onClick(tab, pos.top);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TabNavItemButton active={active}>
|
||||
<User size={14} color={active ? 'rgba(115, 103, 240)' : '#c2c6dc'} />
|
||||
<TabNavItemSpan>{name}</TabNavItemSpan>
|
||||
</TabNavItemButton>
|
||||
</TabNavItem>
|
||||
);
|
||||
const NavItem: React.FC<NavItemProps> = ({ active, name, tab, onClick }) => {
|
||||
const $item = useRef<HTMLLIElement>(null);
|
||||
return (
|
||||
<TabNavItem
|
||||
key={name}
|
||||
ref={$item}
|
||||
onClick={() => {
|
||||
if ($item && $item.current) {
|
||||
const pos = $item.current.getBoundingClientRect();
|
||||
onClick(tab, pos.top);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TabNavItemButton active={active}>
|
||||
<User size={14} color={active ? 'rgba(115, 103, 240)' : '#c2c6dc'} />
|
||||
<TabNavItemSpan>{name}</TabNavItemSpan>
|
||||
</TabNavItemButton>
|
||||
</TabNavItem>
|
||||
);
|
||||
};
|
||||
|
||||
type AdminProps = {
|
||||
initialTab: number;
|
||||
onAddUser: ($target: React.RefObject<HTMLElement>) => void;
|
||||
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
|
||||
onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
|
||||
users: Array<User>;
|
||||
initialTab: number;
|
||||
onAddUser: ($target: React.RefObject<HTMLElement>) => void;
|
||||
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
|
||||
onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
|
||||
users: Array<User>;
|
||||
onUpdateUserPassword: (user: TaskUser, password: string) => void;
|
||||
};
|
||||
|
||||
const Admin: React.FC<AdminProps> = ({initialTab, onAddUser, onDeleteUser, onInviteUser, users}) => {
|
||||
const warning =
|
||||
'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
|
||||
const [currentTop, setTop] = useState(initialTab * 48);
|
||||
const [currentTab, setTab] = useState(initialTab);
|
||||
const {showPopup, hidePopup} = usePopup();
|
||||
const $tabNav = useRef<HTMLDivElement>(null);
|
||||
const Admin: React.FC<AdminProps> = ({ initialTab, onAddUser, onUpdateUserPassword, onDeleteUser, onInviteUser, users }) => {
|
||||
const warning =
|
||||
'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
|
||||
const [currentTop, setTop] = useState(initialTab * 48);
|
||||
const [currentTab, setTab] = useState(initialTab);
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const $tabNav = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [updateUserRole] = useUpdateUserRoleMutation()
|
||||
return (
|
||||
<Container>
|
||||
<TabNav ref={$tabNav}>
|
||||
<TabNavContent>
|
||||
{items.map((item, idx) => (
|
||||
<NavItem
|
||||
onClick={(tab, top) => {
|
||||
if ($tabNav && $tabNav.current) {
|
||||
const pos = $tabNav.current.getBoundingClientRect();
|
||||
setTab(tab);
|
||||
setTop(top - pos.top);
|
||||
}
|
||||
}}
|
||||
name={item.name}
|
||||
tab={idx}
|
||||
active={idx === currentTab}
|
||||
/>
|
||||
))}
|
||||
<TabNavLine top={currentTop} />
|
||||
</TabNavContent>
|
||||
</TabNav>
|
||||
<TabContentWrapper>
|
||||
<TabContent>
|
||||
<MemberListWrapper>
|
||||
<MemberListHeader>
|
||||
<ListTitle>{`Users (${users.length})`}</ListTitle>
|
||||
<ListDesc>
|
||||
Team members can view and join all Team Visible boards and create new boards in the team.
|
||||
</ListDesc>
|
||||
<ListActions>
|
||||
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
||||
<InviteMemberButton
|
||||
onClick={$target => {
|
||||
onAddUser($target);
|
||||
}}
|
||||
>
|
||||
<InviteIcon width={16} height={16} />
|
||||
New Member
|
||||
</InviteMemberButton>
|
||||
</ListActions>
|
||||
</MemberListHeader>
|
||||
<MemberList>
|
||||
{users.map(member => (
|
||||
<MemberListItem>
|
||||
<MemberProfile showRoleIcons size={32} onMemberProfile={() => {}} member={member} />
|
||||
<MemberListItemDetails>
|
||||
<MemberItemName>{member.fullName}</MemberItemName>
|
||||
<MemberItemUsername>{`@${member.username}`}</MemberItemUsername>
|
||||
</MemberListItemDetails>
|
||||
<MemberItemOptions>
|
||||
<MemberItemOption variant="flat">On 6 projects</MemberItemOption>
|
||||
<MemberItemOption
|
||||
variant="outline"
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<TeamRoleManagerPopup
|
||||
user={member}
|
||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||
canChangeRole={member.role && member.role.code !== 'owner'}
|
||||
onChangeRole={roleCode => {
|
||||
updateUserRole({variables: {userID: member.id, roleCode}})
|
||||
}}
|
||||
onRemoveFromTeam={
|
||||
member.role && member.role.code === 'owner'
|
||||
? undefined
|
||||
: () => {
|
||||
hidePopup();
|
||||
const [updateUserRole] = useUpdateUserRoleMutation()
|
||||
return (
|
||||
<Container>
|
||||
<TabNav ref={$tabNav}>
|
||||
<TabNavContent>
|
||||
{items.map((item, idx) => (
|
||||
<NavItem
|
||||
onClick={(tab, top) => {
|
||||
if ($tabNav && $tabNav.current) {
|
||||
const pos = $tabNav.current.getBoundingClientRect();
|
||||
setTab(tab);
|
||||
setTop(top - pos.top);
|
||||
}
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
}}
|
||||
name={item.name}
|
||||
tab={idx}
|
||||
active={idx === currentTab}
|
||||
/>
|
||||
))}
|
||||
<TabNavLine top={currentTop} />
|
||||
</TabNavContent>
|
||||
</TabNav>
|
||||
<TabContentWrapper>
|
||||
<TabContent>
|
||||
<MemberListWrapper>
|
||||
<MemberListHeader>
|
||||
<ListTitle>{`Users (${users.length})`}</ListTitle>
|
||||
<ListDesc>
|
||||
Team members can view and join all Team Visible boards and create new boards in the team.
|
||||
</ListDesc>
|
||||
<ListActions>
|
||||
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
||||
<InviteMemberButton
|
||||
onClick={$target => {
|
||||
onAddUser($target);
|
||||
}}
|
||||
>
|
||||
<InviteIcon width={16} height={16} />
|
||||
New Member
|
||||
</InviteMemberButton>
|
||||
</ListActions>
|
||||
</MemberListHeader>
|
||||
<MemberList>
|
||||
{users.map(member => (
|
||||
<MemberListItem>
|
||||
<MemberProfile showRoleIcons size={32} onMemberProfile={() => { }} member={member} />
|
||||
<MemberListItemDetails>
|
||||
<MemberItemName>{member.fullName}</MemberItemName>
|
||||
<MemberItemUsername>{`@${member.username}`}</MemberItemUsername>
|
||||
</MemberListItemDetails>
|
||||
<MemberItemOptions>
|
||||
<MemberItemOption variant="flat">On 6 projects</MemberItemOption>
|
||||
<MemberItemOption
|
||||
variant="outline"
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<TeamRoleManagerPopup
|
||||
user={member}
|
||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||
updateUserPassword={(user, password) => {
|
||||
onUpdateUserPassword(user, password)
|
||||
}}
|
||||
canChangeRole={member.role && member.role.code !== 'owner'}
|
||||
onChangeRole={roleCode => {
|
||||
updateUserRole({ variables: { userID: member.id, roleCode } })
|
||||
}}
|
||||
onRemoveFromTeam={
|
||||
member.role && member.role.code === 'owner'
|
||||
? undefined
|
||||
: () => {
|
||||
hidePopup();
|
||||
}
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
</MemberItemOption>
|
||||
</MemberItemOptions>
|
||||
</MemberListItem>
|
||||
))}
|
||||
</MemberList>
|
||||
</MemberListWrapper>
|
||||
</TabContent>
|
||||
</TabContentWrapper>
|
||||
</Container>
|
||||
);
|
||||
</MemberItemOptions>
|
||||
</MemberListItem>
|
||||
))}
|
||||
</MemberList>
|
||||
</MemberListWrapper>
|
||||
</TabContent>
|
||||
</TabContentWrapper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Admin;
|
||||
|
7
go.mod
7
go.mod
@ -11,20 +11,25 @@ require (
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/go-chi/chi v3.3.2+incompatible
|
||||
github.com/go-chi/cors v1.0.0
|
||||
github.com/gochrono/chrono v1.1.0
|
||||
github.com/golang-migrate/migrate/v4 v4.11.0
|
||||
github.com/google/martian v2.1.0+incompatible
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e
|
||||
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
|
||||
github.com/lib/pq v1.3.0
|
||||
github.com/magefile/mage v1.9.0
|
||||
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200525100937-58356a36e03f // indirect
|
||||
github.com/pelletier/go-toml v1.8.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spf13/cobra v1.0.0 // indirect
|
||||
github.com/spf13/cobra v1.0.0
|
||||
github.com/spf13/jwalterweatherman v1.0.0
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.4.0
|
||||
github.com/streadway/amqp v1.0.0
|
||||
github.com/vektah/gqlparser/v2 v2.0.1
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
|
||||
|
48
go.sum
48
go.sum
@ -40,6 +40,8 @@ github.com/RichardKnop/machinery v1.8.6 h1:IPdPeO/yVE32isMJME2hiCgJgNibCVLjhshyj
|
||||
github.com/RichardKnop/machinery v1.8.6/go.mod h1:W87mnh7t91WdrwGbdnAjvDzqD/bqBV+0+GF276gv/bU=
|
||||
github.com/RichardKnop/redsync v1.2.0 h1:gK35hR3zZkQigHKm8wOGb9MpJ9BsrW6MzxezwjTcHP0=
|
||||
github.com/RichardKnop/redsync v1.2.0/go.mod h1:9b8nBGAX3bE2uCfJGSnsDvF23mKyHTZzmvmj5FH3Tp0=
|
||||
github.com/SaidinWoT/timespan v0.0.0-20160403210742-a3d8e4741124 h1:TLYKwyrCYBhTXMC9JuFFwtugk9Q2iVixqQKXfxS0TPI=
|
||||
github.com/SaidinWoT/timespan v0.0.0-20160403210742-a3d8e4741124/go.mod h1:C1NXPK9z4q8t533nAdnzlEHucZsRqI66MnPwN8mWKKE=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
@ -72,9 +74,11 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
||||
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
|
||||
github.com/containerd/containerd v1.3.3 h1:LoIzb5y9x5l8VKAlyrbusNPXqBY0+kviRloxFUMFwKc=
|
||||
github.com/containerd/containerd v1.3.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
@ -94,19 +98,27 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/dhui/dktest v0.3.2 h1:nZSDcnkpbotzT/nEHNsO+JCKY8i1Qoki1AYOpeLRb6M=
|
||||
github.com/dhui/dktest v0.3.2/go.mod h1:l1/ib23a/CmxAe7yixtrYPc8Iy90Zy2udyaHINM5p58=
|
||||
github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v1.4.2-0.20200213202729-31a86c4ab209 h1:tmV+YbYOUAYDmAiamzhRKqQXaAUyUY2xVt27Rv7rCzA=
|
||||
github.com/docker/docker v1.4.2-0.20200213202729-31a86c4ab209/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
@ -153,11 +165,16 @@ github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWe
|
||||
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
|
||||
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
|
||||
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
||||
github.com/gochrono/chrono v1.1.0 h1:6dx7rQuSGyAW92xMfN6ZNd+gX4v8a0XTAHEsJ1IDph8=
|
||||
github.com/gochrono/chrono v1.1.0/go.mod h1:W225MA3JCfzO3iAW8lKJm3vrDluqe5JrmqnHkkZfcXc=
|
||||
github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0=
|
||||
github.com/gofrs/uuid v3.1.0+incompatible h1:q2rtkjaKT4YEr6E1kamy0Ha4RtepWlQBedyHx0uzKwA=
|
||||
github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA=
|
||||
github.com/golang-migrate/migrate/v4 v4.11.0 h1:uqtd0ysK5WyBQ/T1K2uDIooJV0o2Obt6uPwP062DupQ=
|
||||
@ -205,6 +222,9 @@ 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 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gookit/color v1.1.6 h1:CisXBwYhzdPZUV+F8J4N3nzTclW78mOYz6TbPwUmhV4=
|
||||
github.com/gookit/color v1.1.6/go.mod h1:655QfvFggjTrC1SaAufon2qad0RLgbdQa40lCeOdU64=
|
||||
github.com/gookit/config v1.0.11/go.mod h1:+0W5UvRpZaChP3aXx0cZ+zv7U9tvX+0oSGjisJA6fWA=
|
||||
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||
@ -228,9 +248,11 @@ github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCO
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
@ -257,6 +279,8 @@ github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jinzhu/now v0.0.0-20180511015916-ed742868f2ae h1:8bBMcboXYVuo0WYH+rPe5mB8obO89a993hdTZ3phTjc=
|
||||
github.com/jinzhu/now v0.0.0-20180511015916-ed742868f2ae/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
||||
@ -274,6 +298,8 @@ github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0Lh
|
||||
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU=
|
||||
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
@ -297,8 +323,10 @@ 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/magefile/mage v1.6.2/go.mod h1:IUDi13rsHje59lecXokTfGX0QIzO45uVPlXnJYsXepA=
|
||||
github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE=
|
||||
github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||
github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
|
||||
@ -326,6 +354,7 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200525100937-58356a36e03f h1:0HN0GKijN4mr9SmYoW/Ni3ozuNeHiSxo2s7drhv7obY=
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200525100937-58356a36e03f/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA=
|
||||
@ -337,7 +366,9 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
|
||||
github.com/onsi/ginkgo v1.10.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
@ -386,9 +417,11 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
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/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo=
|
||||
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=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0 h1:JJV9CsgM9EC9w2iVkwuz+sMx8yRFe89PJRUrv6hPCIA=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
@ -397,16 +430,21 @@ github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo=
|
||||
@ -424,6 +462,7 @@ github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1C
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
@ -433,6 +472,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/vmihailenco/msgpack v4.0.0+incompatible h1:R/ftCULcY/r0SLpalySUSd8QV4fVABi/h0D/IjlYJzg=
|
||||
github.com/vmihailenco/msgpack v4.0.0+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
|
||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk=
|
||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
|
||||
@ -459,6 +500,7 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
|
||||
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-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
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=
|
||||
@ -501,9 +543,11 @@ 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/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180611182652-db08ff08e862/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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181003013248-f5e5bdd77824/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -546,6 +590,7 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -626,6 +671,7 @@ golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapK
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200213224642-88e652f7a869/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200303165918-5bcca83a7881 h1:6bcQ/hWOMu5dXxMPcdxhx5uOoQBkeleqvbGdt4lh8hg=
|
||||
golang.org/x/tools v0.0.0-20200303165918-5bcca83a7881/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -644,6 +690,7 @@ google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/
|
||||
google.golang.org/api v0.19.0 h1:GwFK8+l5/gdsOYKz5p6M4UK+QT8OvmHWZPJCnf+5DjA=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@ -688,6 +735,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
|
40
internal/commands/commands.go
Normal file
40
internal/commands/commands.go
Normal file
@ -0,0 +1,40 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const CitadelConfDirEnvName = "CITADEL_CONFIG_DIR"
|
||||
|
||||
const CitadelAppConf = "citadel"
|
||||
|
||||
const mainDescription = `citadel is an open soure project management
|
||||
system written in Golang & React.`
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
commit = "none"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
var versionTemplate = fmt.Sprintf(`Version: %s
|
||||
Commit: %s
|
||||
Built: %s`, version, commit, date+"\n")
|
||||
|
||||
var commandError error
|
||||
var configDir string
|
||||
var verbose bool
|
||||
var noColor bool
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "citadel",
|
||||
Long: mainDescription,
|
||||
Version: version,
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
rootCmd.SetVersionTemplate(versionTemplate)
|
||||
rootCmd.AddCommand(newWebCmd(), newMigrateCmd())
|
||||
rootCmd.Execute()
|
||||
}
|
68
internal/commands/migrate.go
Normal file
68
internal/commands/migrate.go
Normal file
@ -0,0 +1,68 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/jordanknott/project-citadel/api/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type MigrateLog struct {
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func (l *MigrateLog) Printf(format string, v ...interface{}) {
|
||||
log.Printf("%s", v)
|
||||
}
|
||||
|
||||
// Verbose shows if verbose print enabled
|
||||
func (l *MigrateLog) Verbose() bool {
|
||||
return l.verbose
|
||||
}
|
||||
|
||||
func newMigrateCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "migrate",
|
||||
Short: "Run the database schema migrations",
|
||||
Long: "Run the database schema migrations",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
appConfig, err := config.LoadConfig("conf/app.toml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable",
|
||||
appConfig.Database.User,
|
||||
appConfig.Database.Password,
|
||||
appConfig.Database.Host,
|
||||
appConfig.Database.Name,
|
||||
)
|
||||
db, err := sqlx.Connect("postgres", connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
driver, err := postgres.WithInstance(db.DB, &postgres.Config{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m, err := migrate.NewWithDatabaseInstance(
|
||||
"file://migrations",
|
||||
"postgres", driver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger := &MigrateLog{}
|
||||
m.Log = logger
|
||||
err = m.Up()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
52
internal/commands/web.go
Normal file
52
internal/commands/web.go
Normal file
@ -0,0 +1,52 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/jordanknott/project-citadel/api/internal/config"
|
||||
"github.com/jordanknott/project-citadel/api/internal/route"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func newWebCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "web",
|
||||
Short: "Run the web server",
|
||||
Long: "Run the web & api server",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
appConfig, err := config.LoadConfig("conf/app.toml")
|
||||
if err != nil {
|
||||
log.WithError(err).Error("loading config")
|
||||
}
|
||||
Formatter := new(log.TextFormatter)
|
||||
Formatter.TimestampFormat = "02-01-2006 15:04:05"
|
||||
Formatter.FullTimestamp = true
|
||||
log.SetFormatter(Formatter)
|
||||
log.SetLevel(log.InfoLevel)
|
||||
|
||||
connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable",
|
||||
appConfig.Database.User,
|
||||
appConfig.Database.Password,
|
||||
appConfig.Database.Host,
|
||||
appConfig.Database.Name,
|
||||
)
|
||||
db, err := sqlx.Connect("postgres", connection)
|
||||
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxIdleConns(25)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
|
||||
defer db.Close()
|
||||
log.WithFields(log.Fields{"url": appConfig.General.Host}).Info("starting server")
|
||||
r, _ := route.NewRouter(appConfig, db)
|
||||
http.ListenAndServe(appConfig.General.Host, r)
|
||||
},
|
||||
}
|
||||
}
|
58
internal/config/config.go
Normal file
58
internal/config/config.go
Normal file
@ -0,0 +1,58 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/BurntSushi/toml"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
Host string
|
||||
Name string
|
||||
User string
|
||||
Password string
|
||||
}
|
||||
|
||||
type General struct {
|
||||
Host string
|
||||
}
|
||||
|
||||
type EmailNotifications struct {
|
||||
Enabled bool
|
||||
DisplayName string `toml:"display_name"`
|
||||
FromAddress string `toml:"from_address"`
|
||||
}
|
||||
|
||||
type Storage struct {
|
||||
StorageSystem string `toml:"local_storage"`
|
||||
UploadDirPath string `toml:"upload_dir_path"`
|
||||
}
|
||||
|
||||
type Smtp struct {
|
||||
Username string
|
||||
Password string
|
||||
Server string
|
||||
Port int
|
||||
ConnectionSecurity string `toml:"connection_security"`
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
General General
|
||||
Database Database
|
||||
EmailNotifications EmailNotifications `toml:"email_notifications"`
|
||||
Storage Storage
|
||||
Smtp Smtp
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (AppConfig, error) {
|
||||
dat, err := ioutil.ReadFile("conf/app.toml")
|
||||
if err != nil {
|
||||
return AppConfig{}, err
|
||||
}
|
||||
|
||||
var appConfig AppConfig
|
||||
_, err = toml.Decode(string(dat), &appConfig)
|
||||
if err != nil {
|
||||
return AppConfig{}, err
|
||||
}
|
||||
return appConfig, nil
|
||||
}
|
@ -83,6 +83,7 @@ type Querier interface {
|
||||
SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams) (Task, error)
|
||||
SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error)
|
||||
SetTeamOwner(ctx context.Context, arg SetTeamOwnerParams) (Team, error)
|
||||
SetUserPassword(ctx context.Context, arg SetUserPasswordParams) (UserAccount, error)
|
||||
UpdateProjectLabel(ctx context.Context, arg UpdateProjectLabelParams) (ProjectLabel, error)
|
||||
UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error)
|
||||
UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error)
|
||||
|
@ -25,3 +25,6 @@ WHERE user_id = $1;
|
||||
|
||||
-- name: UpdateUserRole :one
|
||||
UPDATE user_account SET role_code = $2 WHERE user_id = $1 RETURNING *;
|
||||
|
||||
-- name: SetUserPassword :one
|
||||
UPDATE user_account SET password_hash = $2 WHERE user_id = $1 RETURNING *;
|
||||
|
@ -162,6 +162,33 @@ func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const setUserPassword = `-- name: SetUserPassword :one
|
||||
UPDATE user_account SET password_hash = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code
|
||||
`
|
||||
|
||||
type SetUserPasswordParams struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
}
|
||||
|
||||
func (q *Queries) SetUserPassword(ctx context.Context, arg SetUserPasswordParams) (UserAccount, error) {
|
||||
row := q.db.QueryRowContext(ctx, setUserPassword, arg.UserID, arg.PasswordHash)
|
||||
var i UserAccount
|
||||
err := row.Scan(
|
||||
&i.UserID,
|
||||
&i.CreatedAt,
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ProfileBgColor,
|
||||
&i.FullName,
|
||||
&i.Initials,
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateUserAccountProfileAvatarURL = `-- name: UpdateUserAccountProfileAvatarURL :one
|
||||
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
|
||||
RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code
|
||||
|
@ -185,6 +185,7 @@ type ComplexityRoot struct {
|
||||
UpdateTaskLocation func(childComplexity int, input NewTaskLocation) int
|
||||
UpdateTaskName func(childComplexity int, input UpdateTaskName) int
|
||||
UpdateTeamMemberRole func(childComplexity int, input UpdateTeamMemberRole) int
|
||||
UpdateUserPassword func(childComplexity int, input UpdateUserPassword) int
|
||||
UpdateUserRole func(childComplexity int, input UpdateUserRole) int
|
||||
}
|
||||
|
||||
@ -347,6 +348,11 @@ type ComplexityRoot struct {
|
||||
Ok func(childComplexity int) int
|
||||
}
|
||||
|
||||
UpdateUserPasswordPayload struct {
|
||||
Ok func(childComplexity int) int
|
||||
User func(childComplexity int) int
|
||||
}
|
||||
|
||||
UpdateUserRolePayload struct {
|
||||
User func(childComplexity int) int
|
||||
}
|
||||
@ -415,6 +421,7 @@ type MutationResolver interface {
|
||||
DeleteUserAccount(ctx context.Context, input DeleteUserAccount) (*DeleteUserAccountPayload, error)
|
||||
LogoutUser(ctx context.Context, input LogoutUser) (bool, error)
|
||||
ClearProfileAvatar(ctx context.Context) (*db.UserAccount, error)
|
||||
UpdateUserPassword(ctx context.Context, input UpdateUserPassword) (*UpdateUserPasswordPayload, error)
|
||||
UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error)
|
||||
}
|
||||
type OrganizationResolver interface {
|
||||
@ -1341,6 +1348,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.Mutation.UpdateTeamMemberRole(childComplexity, args["input"].(UpdateTeamMemberRole)), true
|
||||
|
||||
case "Mutation.updateUserPassword":
|
||||
if e.complexity.Mutation.UpdateUserPassword == nil {
|
||||
break
|
||||
}
|
||||
|
||||
args, err := ec.field_Mutation_updateUserPassword_args(context.TODO(), rawArgs)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return e.complexity.Mutation.UpdateUserPassword(childComplexity, args["input"].(UpdateUserPassword)), true
|
||||
|
||||
case "Mutation.updateUserRole":
|
||||
if e.complexity.Mutation.UpdateUserRole == nil {
|
||||
break
|
||||
@ -2008,6 +2027,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.UpdateTeamMemberRolePayload.Ok(childComplexity), true
|
||||
|
||||
case "UpdateUserPasswordPayload.ok":
|
||||
if e.complexity.UpdateUserPasswordPayload.Ok == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.UpdateUserPasswordPayload.Ok(childComplexity), true
|
||||
|
||||
case "UpdateUserPasswordPayload.user":
|
||||
if e.complexity.UpdateUserPasswordPayload.User == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.UpdateUserPasswordPayload.User(childComplexity), true
|
||||
|
||||
case "UpdateUserRolePayload.user":
|
||||
if e.complexity.UpdateUserRolePayload.User == nil {
|
||||
break
|
||||
@ -2707,9 +2740,20 @@ extend type Mutation {
|
||||
logoutUser(input: LogoutUser!): Boolean!
|
||||
clearProfileAvatar: UserAccount!
|
||||
|
||||
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
||||
updateUserRole(input: UpdateUserRole!): UpdateUserRolePayload!
|
||||
}
|
||||
|
||||
input UpdateUserPassword {
|
||||
userID: UUID!
|
||||
password: String!
|
||||
}
|
||||
|
||||
type UpdateUserPasswordPayload {
|
||||
ok: Boolean!
|
||||
user: UserAccount!
|
||||
}
|
||||
|
||||
input UpdateUserRole {
|
||||
userID: UUID!
|
||||
roleCode: RoleCode!
|
||||
@ -3411,6 +3455,20 @@ func (ec *executionContext) field_Mutation_updateTeamMemberRole_args(ctx context
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_updateUserPassword_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||
var err error
|
||||
args := map[string]interface{}{}
|
||||
var arg0 UpdateUserPassword
|
||||
if tmp, ok := rawArgs["input"]; ok {
|
||||
arg0, err = ec.unmarshalNUpdateUserPassword2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐUpdateUserPassword(ctx, tmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
args["input"] = arg0
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_updateUserRole_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||
var err error
|
||||
args := map[string]interface{}{}
|
||||
@ -6761,6 +6819,47 @@ func (ec *executionContext) _Mutation_clearProfileAvatar(ctx context.Context, fi
|
||||
return ec.marshalNUserAccount2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋdbᚐUserAccount(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Mutation_updateUserPassword(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "Mutation",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: true,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
rawArgs := field.ArgumentMap(ec.Variables)
|
||||
args, err := ec.field_Mutation_updateUserPassword_args(ctx, rawArgs)
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
fc.Args = args
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return ec.resolvers.Mutation().UpdateUserPassword(rctx, args["input"].(UpdateUserPassword))
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*UpdateUserPasswordPayload)
|
||||
fc.Result = res
|
||||
return ec.marshalNUpdateUserPasswordPayload2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐUpdateUserPasswordPayload(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Mutation_updateUserRole(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@ -9945,6 +10044,74 @@ func (ec *executionContext) _UpdateTeamMemberRolePayload_member(ctx context.Cont
|
||||
return ec.marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐMember(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _UpdateUserPasswordPayload_ok(ctx context.Context, field graphql.CollectedField, obj *UpdateUserPasswordPayload) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "UpdateUserPasswordPayload",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: false,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.Ok, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(bool)
|
||||
fc.Result = res
|
||||
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _UpdateUserPasswordPayload_user(ctx context.Context, field graphql.CollectedField, obj *UpdateUserPasswordPayload) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "UpdateUserPasswordPayload",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: false,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.User, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*db.UserAccount)
|
||||
fc.Result = res
|
||||
return ec.marshalNUserAccount2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋdbᚐUserAccount(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _UpdateUserRolePayload_user(ctx context.Context, field graphql.CollectedField, obj *UpdateUserRolePayload) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@ -12554,6 +12721,30 @@ func (ec *executionContext) unmarshalInputUpdateTeamMemberRole(ctx context.Conte
|
||||
return it, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalInputUpdateUserPassword(ctx context.Context, obj interface{}) (UpdateUserPassword, error) {
|
||||
var it UpdateUserPassword
|
||||
var asMap = obj.(map[string]interface{})
|
||||
|
||||
for k, v := range asMap {
|
||||
switch k {
|
||||
case "userID":
|
||||
var err error
|
||||
it.UserID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "password":
|
||||
var err error
|
||||
it.Password, err = ec.unmarshalNString2string(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return it, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalInputUpdateUserRole(ctx context.Context, obj interface{}) (UpdateUserRole, error) {
|
||||
var it UpdateUserRole
|
||||
var asMap = obj.(map[string]interface{})
|
||||
@ -13340,6 +13531,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "updateUserPassword":
|
||||
out.Values[i] = ec._Mutation_updateUserPassword(ctx, field)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "updateUserRole":
|
||||
out.Values[i] = ec._Mutation_updateUserRole(ctx, field)
|
||||
if out.Values[i] == graphql.Null {
|
||||
@ -14668,6 +14864,38 @@ func (ec *executionContext) _UpdateTeamMemberRolePayload(ctx context.Context, se
|
||||
return out
|
||||
}
|
||||
|
||||
var updateUserPasswordPayloadImplementors = []string{"UpdateUserPasswordPayload"}
|
||||
|
||||
func (ec *executionContext) _UpdateUserPasswordPayload(ctx context.Context, sel ast.SelectionSet, obj *UpdateUserPasswordPayload) graphql.Marshaler {
|
||||
fields := graphql.CollectFields(ec.OperationContext, sel, updateUserPasswordPayloadImplementors)
|
||||
|
||||
out := graphql.NewFieldSet(fields)
|
||||
var invalids uint32
|
||||
for i, field := range fields {
|
||||
switch field.Name {
|
||||
case "__typename":
|
||||
out.Values[i] = graphql.MarshalString("UpdateUserPasswordPayload")
|
||||
case "ok":
|
||||
out.Values[i] = ec._UpdateUserPasswordPayload_ok(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "user":
|
||||
out.Values[i] = ec._UpdateUserPasswordPayload_user(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
}
|
||||
out.Dispatch()
|
||||
if invalids > 0 {
|
||||
return graphql.Null
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var updateUserRolePayloadImplementors = []string{"UpdateUserRolePayload"}
|
||||
|
||||
func (ec *executionContext) _UpdateUserRolePayload(ctx context.Context, sel ast.SelectionSet, obj *UpdateUserRolePayload) graphql.Marshaler {
|
||||
@ -16230,6 +16458,24 @@ func (ec *executionContext) marshalNUpdateTeamMemberRolePayload2ᚖgithubᚗcom
|
||||
return ec._UpdateTeamMemberRolePayload(ctx, sel, v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalNUpdateUserPassword2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐUpdateUserPassword(ctx context.Context, v interface{}) (UpdateUserPassword, error) {
|
||||
return ec.unmarshalInputUpdateUserPassword(ctx, v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalNUpdateUserPasswordPayload2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐUpdateUserPasswordPayload(ctx context.Context, sel ast.SelectionSet, v UpdateUserPasswordPayload) graphql.Marshaler {
|
||||
return ec._UpdateUserPasswordPayload(ctx, sel, &v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalNUpdateUserPasswordPayload2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐUpdateUserPasswordPayload(ctx context.Context, sel ast.SelectionSet, v *UpdateUserPasswordPayload) graphql.Marshaler {
|
||||
if v == nil {
|
||||
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
return ec._UpdateUserPasswordPayload(ctx, sel, v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalNUpdateUserRole2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐUpdateUserRole(ctx context.Context, v interface{}) (UpdateUserRole, error) {
|
||||
return ec.unmarshalInputUpdateUserRole(ctx, v)
|
||||
}
|
||||
|
@ -12,13 +12,15 @@ import (
|
||||
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||
"github.com/99designs/gqlgen/graphql/playground"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jordanknott/project-citadel/api/internal/config"
|
||||
"github.com/jordanknott/project-citadel/api/internal/db"
|
||||
)
|
||||
|
||||
// NewHandler returns a new graphql endpoint handler.
|
||||
func NewHandler(repo db.Repository) http.Handler {
|
||||
func NewHandler(config config.AppConfig, repo db.Repository) http.Handler {
|
||||
srv := handler.New(NewExecutableSchema(Config{
|
||||
Resolvers: &Resolver{
|
||||
Config: config,
|
||||
Repository: repo,
|
||||
},
|
||||
}))
|
||||
|
@ -401,6 +401,16 @@ type UpdateTeamMemberRolePayload struct {
|
||||
Member *Member `json:"member"`
|
||||
}
|
||||
|
||||
type UpdateUserPassword struct {
|
||||
UserID uuid.UUID `json:"userID"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type UpdateUserPasswordPayload struct {
|
||||
Ok bool `json:"ok"`
|
||||
User *db.UserAccount `json:"user"`
|
||||
}
|
||||
|
||||
type UpdateUserRole struct {
|
||||
UserID uuid.UUID `json:"userID"`
|
||||
RoleCode RoleCode `json:"roleCode"`
|
||||
|
@ -5,10 +5,12 @@ package graph
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/jordanknott/project-citadel/api/internal/config"
|
||||
"github.com/jordanknott/project-citadel/api/internal/db"
|
||||
)
|
||||
|
||||
type Resolver struct {
|
||||
Config config.AppConfig
|
||||
Repository db.Repository
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
@ -570,9 +570,20 @@ extend type Mutation {
|
||||
logoutUser(input: LogoutUser!): Boolean!
|
||||
clearProfileAvatar: UserAccount!
|
||||
|
||||
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
||||
updateUserRole(input: UpdateUserRole!): UpdateUserRolePayload!
|
||||
}
|
||||
|
||||
input UpdateUserPassword {
|
||||
userID: UUID!
|
||||
password: String!
|
||||
}
|
||||
|
||||
type UpdateUserPasswordPayload {
|
||||
ok: Boolean!
|
||||
user: UserAccount!
|
||||
}
|
||||
|
||||
input UpdateUserRole {
|
||||
userID: UUID!
|
||||
roleCode: RoleCode!
|
||||
|
@ -783,13 +783,24 @@ func (r *mutationResolver) ClearProfileAvatar(ctx context.Context) (*db.UserAcco
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) UpdateUserPassword(ctx context.Context, input UpdateUserPassword) (*UpdateUserPasswordPayload, error) {
|
||||
hashedPwd, err := bcrypt.GenerateFromPassword([]byte(input.Password), 14)
|
||||
if err != nil {
|
||||
return &UpdateUserPasswordPayload{}, err
|
||||
}
|
||||
user, err := r.Repository.SetUserPassword(ctx, db.SetUserPasswordParams{UserID: input.UserID, PasswordHash: string(hashedPwd)})
|
||||
if err != nil {
|
||||
return &UpdateUserPasswordPayload{}, err
|
||||
}
|
||||
return &UpdateUserPasswordPayload{Ok: true, User: &user}, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error) {
|
||||
user, err := r.Repository.UpdateUserRole(ctx, db.UpdateUserRoleParams{RoleCode: input.RoleCode.String(), UserID: input.UserID})
|
||||
if err != nil {
|
||||
return &UpdateUserRolePayload{}, err
|
||||
}
|
||||
return &UpdateUserRolePayload{User: &user}, nil
|
||||
|
||||
}
|
||||
|
||||
func (r *organizationResolver) ID(ctx context.Context, obj *db.Organization) (uuid.UUID, error) {
|
||||
|
@ -5,9 +5,20 @@ extend type Mutation {
|
||||
logoutUser(input: LogoutUser!): Boolean!
|
||||
clearProfileAvatar: UserAccount!
|
||||
|
||||
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
||||
updateUserRole(input: UpdateUserRole!): UpdateUserRolePayload!
|
||||
}
|
||||
|
||||
input UpdateUserPassword {
|
||||
userID: UUID!
|
||||
password: String!
|
||||
}
|
||||
|
||||
type UpdateUserPasswordPayload {
|
||||
ok: Boolean!
|
||||
user: UserAccount!
|
||||
}
|
||||
|
||||
input UpdateUserRole {
|
||||
userID: UUID!
|
||||
roleCode: RoleCode!
|
||||
|
@ -5,13 +5,27 @@ import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/jordanknott/project-citadel/api/internal/db"
|
||||
"github.com/jordanknott/project-citadel/api/internal/frontend"
|
||||
)
|
||||
|
||||
func (h *CitadelHandler) Frontend(w http.ResponseWriter, r *http.Request) {
|
||||
f, err := frontend.Frontend.Open("index.h")
|
||||
if os.IsNotExist(err) {
|
||||
log.Warning("does not exist")
|
||||
} else if err != nil {
|
||||
log.WithError(err).Error("frontend")
|
||||
}
|
||||
http.ServeContent(w, r, "index.html", time.Now(), f)
|
||||
}
|
||||
|
||||
func (h *CitadelHandler) ProfileImageUpload(w http.ResponseWriter, r *http.Request) {
|
||||
log.Info("preparing to upload file")
|
||||
userID, ok := r.Context().Value("userID").(uuid.UUID)
|
||||
|
@ -10,16 +10,61 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/jordanknott/project-citadel/api/internal/config"
|
||||
"github.com/jordanknott/project-citadel/api/internal/db"
|
||||
"github.com/jordanknott/project-citadel/api/internal/frontend"
|
||||
"github.com/jordanknott/project-citadel/api/internal/graph"
|
||||
"github.com/jordanknott/project-citadel/api/internal/logger"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type CitadelHandler struct {
|
||||
repo db.Repository
|
||||
// spaHandler implements the http.Handler interface, so we can use it
|
||||
// to respond to HTTP requests. The path to the static directory and
|
||||
// path to the index file within that static directory are used to
|
||||
// serve the SPA in the given static directory.
|
||||
type FrontendHandler struct {
|
||||
staticPath string
|
||||
indexPath string
|
||||
}
|
||||
|
||||
func NewRouter(dbConnection *sqlx.DB) (chi.Router, error) {
|
||||
func IsDir(f http.File) bool {
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return fi.IsDir()
|
||||
}
|
||||
|
||||
func (h FrontendHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
path, err := filepath.Abs(r.URL.Path)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
f, err := frontend.Frontend.Open(path)
|
||||
if os.IsNotExist(err) || IsDir(f) {
|
||||
index, err := frontend.Frontend.Open("index.html")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.ServeContent(w, r, "index.html", time.Now(), index)
|
||||
return
|
||||
} else if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.ServeContent(w, r, path, time.Now(), f)
|
||||
}
|
||||
|
||||
type CitadelHandler struct {
|
||||
config config.AppConfig
|
||||
repo db.Repository
|
||||
}
|
||||
|
||||
func NewRouter(config config.AppConfig, dbConnection *sqlx.DB) (chi.Router, error) {
|
||||
formatter := new(log.TextFormatter)
|
||||
formatter.TimestampFormat = "02-01-2006 15:04:05"
|
||||
formatter.FullTimestamp = true
|
||||
@ -47,7 +92,7 @@ func NewRouter(dbConnection *sqlx.DB) (chi.Router, error) {
|
||||
r.Use(middleware.Timeout(60 * time.Second))
|
||||
|
||||
repository := db.NewRepository(dbConnection)
|
||||
citadelHandler := CitadelHandler{*repository}
|
||||
citadelHandler := CitadelHandler{config, *repository}
|
||||
|
||||
var imgServer = http.FileServer(http.Dir("./uploads/"))
|
||||
r.Group(func(mux chi.Router) {
|
||||
@ -59,8 +104,11 @@ func NewRouter(dbConnection *sqlx.DB) (chi.Router, error) {
|
||||
r.Group(func(mux chi.Router) {
|
||||
mux.Use(AuthenticationMiddleware)
|
||||
mux.Post("/users/me/avatar", citadelHandler.ProfileImageUpload)
|
||||
mux.Handle("/graphql", graph.NewHandler(*repository))
|
||||
mux.Handle("/graphql", graph.NewHandler(config, *repository))
|
||||
})
|
||||
|
||||
frontend := FrontendHandler{staticPath: "build", indexPath: "index.html"}
|
||||
r.Handle("/*", frontend)
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
15
magefile.go
15
magefile.go
@ -5,7 +5,9 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/magefile/mage/sh"
|
||||
"github.com/shurcooL/vfsgen"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
@ -14,6 +16,19 @@ var Aliases = map[string]interface{}{
|
||||
"g": Generate,
|
||||
}
|
||||
|
||||
func Vfs() error {
|
||||
var fs http.FileSystem = http.Dir("frontend/build")
|
||||
err := vfsgen.Generate(fs, vfsgen.Options{
|
||||
Filename: "internal/frontend/frontend_generated.go",
|
||||
PackageName: "frontend",
|
||||
VariableName: "Frontend",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Runs go mod download and then installs the binary.
|
||||
func Generate() error {
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user