Create tabls in profiles page (#137)

* Create tab for signing profiles in profiles page

* Add rpc methods to create and get contact profiles

* Create tab for contact profiles

* Create page for create profile

* Fix submit create contact profile form

* Fix get contact profiles request in profiles page

* Fix validation of squeak address in create contact method

* Validate squeak address in create contact profile frontend
This commit is contained in:
Jonathan Zernik 2020-07-24 22:43:39 -07:00 committed by GitHub
parent e1ff407598
commit 441aecd148
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 437 additions and 32 deletions

View file

@ -20,6 +20,7 @@ import Dashboard from "../../pages/dashboard";
import SqueakAddress from "../../pages/squeakaddress";
import Profile from "../../pages/profile";
import CreateSigningProfile from "../../pages/createsigningprofile";
import CreateContactProfile from "../../pages/createcontactprofile";
import MakeSqueak from "../../pages/makesqueak";
import Lightning from "../../pages/lightning";
import Notifications from "../../pages/notifications";
@ -54,6 +55,7 @@ function Layout(props) {
<Route path="/app/squeakaddress/:address" component={SqueakAddress} />
<Route path="/app/profile/:id" component={Profile} />
<Route path="/app/createsigningprofile" component={CreateSigningProfile} />
<Route path="/app/createcontactprofile" component={CreateContactProfile} />
<Route path="/app/profiles" component={Profiles} />
<Route path="/app/makesqueak" component={MakeSqueak} />
<Route path="/app/lightning" component={Lightning} />

View file

@ -0,0 +1,83 @@
import React, {useState, useEffect} from 'react';
import {useHistory} from "react-router-dom";
import {Grid, TextField, Button, Typography, Paper} from "@material-ui/core";
// styles
import useStyles from "./styles";
// components
import PageTitle from "../../components/PageTitle";
import Widget from "../../components/Widget";
// import { Typography } from "../../components/Wrappers";
import {CreateContactProfileRequest} from "../../proto/squeak_admin_pb"
import {SqueakAdminClient} from "../../proto/squeak_admin_grpc_web_pb"
var client = new SqueakAdminClient('http://' + window.location.hostname + ':8080')
export default function CreateContactProfilePage() {
const [profileName, setProfileName] = useState('');
const [address, setAddress] = useState('');
var classes = useStyles();
const history = useHistory();
const goToProfilePage = (profileId) => {
history.push("/app/profile/" + profileId);
};
function handleSubmit(event) {
event.preventDefault();
console.log( 'profileName:', profileName);
console.log( 'address:', address);
// You should see email and password in console.
// ..code to submit form to backend here...
createContactProfile(profileName, address);
}
const createContactProfile = (profileName, squeakAddress) => {
console.log("called createContactProfile");
var createContactProfileRequest = new CreateContactProfileRequest()
createContactProfileRequest.setProfileName(profileName);
createContactProfileRequest.setAddress(squeakAddress);
console.log(createContactProfileRequest);
client.createContactProfile(createContactProfileRequest, {}, (err, response) => {
if (err) {
console.log(err.message);
alert('Error creating contact profile: ' + err.message);
return;
}
console.log(response);
console.log(response.getProfileId());
goToProfilePage(response.getProfileId());
});
};
return (
<>
< PageTitle title = "Create Contact Profile" />
<Paper>
<form className={classes.root} onSubmit={handleSubmit} >
<TextField required
value={profileName}
label="Profile name"
onInput={ e=>setProfileName(e.target.value)}
/>
<TextField required
value={address}
label="Address"
onInput={ e=>setAddress(e.target.value)}
/>
<Typography className={classes.divider} />
<Button
type="submit"
className={classes.button}
>
Create
</Button>
</form>
</Paper>
</>);
}

View file

@ -0,0 +1,6 @@
{
"name": "CreateContactProfile",
"version": "0.0.0",
"main": "CreateContactProfile.js",
"private": true
}

View file

@ -0,0 +1,21 @@
import { makeStyles } from "@material-ui/styles";
export default makeStyles(theme => ({
dashedBorder: {
border: "1px dashed",
borderColor: theme.palette.primary.main,
padding: theme.spacing(2),
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(4),
marginTop: theme.spacing(1),
},
text: {
marginBottom: theme.spacing(2),
},
root: {
'& .MuiTextField-root': {
margin: theme.spacing(1),
width: '25ch',
},
},
}));

View file

@ -41,6 +41,11 @@ export default function CreateSigningProfilePage() {
console.log(createSigningProfileRequest);
client.createSigningProfile(createSigningProfileRequest, {}, (err, response) => {
if (err) {
console.log(err.message);
alert('Error creating signing profile: ' + err.message);
return;
}
console.log(response);
console.log(response.getProfileId());
goToProfilePage(response.getProfileId());

View file

@ -1,6 +1,15 @@
import React, {useState, useEffect} from 'react';
import {useHistory} from "react-router-dom";
import {Grid, Button} from "@material-ui/core";
import {
Grid,
Button,
Paper,
Tabs,
Tab,
AppBar,
Box,
Typography,
} from "@material-ui/core";
import MUIDataTable from "mui-datatables";
// styles
@ -15,7 +24,12 @@ import Table from "../dashboard/components/Table/Table";
import mock from "../dashboard/mock";
import {GetInfoRequest} from "../../proto/lnd_pb"
import {HelloRequest, GetFollowedSqueakDisplaysRequest, GetSigningProfilesRequest} from "../../proto/squeak_admin_pb"
import {
HelloRequest,
GetFollowedSqueakDisplaysRequest,
GetSigningProfilesRequest,
GetContactProfilesRequest,
} from "../../proto/squeak_admin_pb"
import {SqueakAdminClient} from "../../proto/squeak_admin_grpc_web_pb"
var client = new SqueakAdminClient('http://' + window.location.hostname + ':8080')
@ -85,8 +99,21 @@ const useStyles = makeStyles((theme) => ({
export default function Profiles() {
const classes = useStyles();
const [signingProfiles, setSigningProfiles] = useState([]);
const [contactProfiles, setContactProfiles] = useState([]);
const [value, setValue] = useState(0);
const history = useHistory();
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
const handleChange = (event, newValue) => {
setValue(newValue);
};
const getLndInfo = () => {
console.log("called getLndInfo");
@ -114,11 +141,29 @@ export default function Profiles() {
setSigningProfiles(response.getSqueakProfilesList());
});
};
const getContactProfiles = () => {
console.log("called getContactProfiles");
var getContactProfilesRequest = new GetContactProfilesRequest()
client.getContactProfiles(getContactProfilesRequest, {}, (err, response) => {
if (err) {
console.log(err.message);
return;
}
console.log(response);
setContactProfiles(response.getSqueakProfilesList());
});
};
const goToCreateSigningProfilePage = () => {
history.push("/app/createsigningprofile");
};
const goToCreateContactProfilePage = () => {
history.push("/app/createcontactprofile");
};
const goToSqueakAddressPage = (squeakAddress) => {
history.push("/app/squeakaddress/" + squeakAddress);
};
@ -129,11 +174,52 @@ export default function Profiles() {
useEffect(() => {
getSigningProfiles()
}, []);
useEffect(() => {
getContactProfiles()
}, []);
return (
<>
< PageTitle title = "Profiles" />
<Grid container spacing={4}>
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box p={3}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}
function ProfilesTabs() {
return (
<>
<AppBar position="static" color="default">
<Tabs value={value} onChange={handleChange} aria-label="simple tabs example">
<Tab label="Signing Profiles" {...a11yProps(0)} />
<Tab label="Contact Profiles" {...a11yProps(1)} />
</Tabs>
</AppBar>
<TabPanel value={value} index={0}>
{SigningProfiles()}
</TabPanel>
<TabPanel value={value} index={1}>
{ContactProfiles()}
</TabPanel>
</>
)
}
function CreateSigningProfileButton() {
return (
<>
<Grid item xs={12}>
<div className={classes.root}>
<Button
@ -142,37 +228,98 @@ export default function Profiles() {
goToCreateSigningProfilePage();
}}>Create Signing Profile
</Button>
</div>
</Grid>
</>
)
}
function CreateContactProfileButton() {
return (
<>
<Grid item xs={12}>
<div className={classes.root}>
<Button
variant="contained"
onClick={() => {
alert('Add contact button clicked')
goToCreateContactProfilePage();
}}>Add contact
</Button>
</div>
</Grid>
<Grid item xs={12}>
<MUIDataTable
title="Profile List"
data={signingProfiles.map(p =>
[
p.getProfileName(),
p.getAddress(),
p.getFollowing().toString(),
p.getSharing().toString(),
]
)}
columns={["Name", "Address", "Following", "Sharing"]}
options={{
filter: false,
print: false,
viewColumns: false,
selectableRows: "none",
onRowClick: rowData => {
var address = rowData[1];
goToSqueakAddressPage(address);
}
}}/>
</Grid>
</Grid>
</>
)
}
function SigningProfiles() {
return (
<>
<Grid container spacing={4}>
{CreateSigningProfileButton()}
<Grid item xs={12}>
<MUIDataTable
title="Signing Profiles"
data={signingProfiles.map(p =>
[
p.getProfileName(),
p.getAddress(),
p.getFollowing().toString(),
p.getSharing().toString(),
]
)}
columns={["Name", "Address", "Following", "Sharing"]}
options={{
filter: false,
print: false,
viewColumns: false,
selectableRows: "none",
onRowClick: rowData => {
var address = rowData[1];
goToSqueakAddressPage(address);
}
}}/>
</Grid>
</Grid>
</>
)
}
function ContactProfiles() {
return (
<>
<Grid container spacing={4}>
{CreateContactProfileButton()}
<Grid item xs={12}>
<MUIDataTable
title="Contact Profiles"
data={contactProfiles.map(p =>
[
p.getProfileName(),
p.getAddress(),
p.getFollowing().toString(),
p.getSharing().toString(),
]
)}
columns={["Name", "Address", "Following", "Sharing"]}
options={{
filter: false,
print: false,
viewColumns: false,
selectableRows: "none",
onRowClick: rowData => {
var address = rowData[1];
goToSqueakAddressPage(address);
}
}}/>
</Grid>
</Grid>
</>
)
}
return (
<>
< PageTitle title = "Profiles" />
{ProfilesTabs()}
< />);
}

View file

@ -261,6 +261,21 @@ def run():
)
profile_id = create_signing_profile_response.profile_id
# Create a new contact profile
contact_name = "carol"
contact_address = "1GbFEcAaAzi2fGRaTsgDMm4N8cue5P26mc"
create_contact_profile_response = admin_stub.CreateContactProfile(
squeak_admin_pb2.CreateContactProfileRequest(
profile_name=contact_name,
address=contact_address,
)
)
print(
"Get create contact profile response: "
+ str(create_contact_profile_response)
)
contact_profile_id = create_contact_profile_response.profile_id
# Get the new squeak profile
get_squeak_profile_response = admin_stub.GetSqueakProfile(
squeak_admin_pb2.GetSqueakProfileRequest(profile_id=profile_id,)
@ -342,6 +357,15 @@ def run():
len(get_signing_profiles_response.squeak_profiles) == 1
)
# Get all contact profiles
get_contact_profiles_response = admin_stub.GetContactProfiles(
squeak_admin_pb2.GetContactProfilesRequest()
)
print("Get contact profiles response: " + str(get_contact_profiles_response))
assert (
len(get_contact_profiles_response.squeak_profiles) == 1
)
# Get all squeak displays for the known address
get_address_squeak_display_response = admin_stub.GetAddressSqueakDisplays(
squeak_admin_pb2.GetAddressSqueakDisplaysRequest(

View file

@ -26,10 +26,18 @@ service SqueakAdmin {
*/
rpc CreateSigningProfile (CreateSigningProfileRequest) returns (CreateSigningProfileReply) {}
/** sqkadmin: `createcontactprofile`
*/
rpc CreateContactProfile (CreateContactProfileRequest) returns (CreateContactProfileReply) {}
/** sqkadmin: `getsigningprofiles`
*/
rpc GetSigningProfiles (GetSigningProfilesRequest) returns (GetSigningProfilesReply) {}
/** sqkadmin: `getcontactprofiles`
*/
rpc GetContactProfiles (GetContactProfilesRequest) returns (GetContactProfilesReply) {}
/** sqkadmin: `getsqueakprofile`
*/
rpc GetSqueakProfile (GetSqueakProfileRequest) returns (GetSqueakProfileReply) {}
@ -74,6 +82,19 @@ message CreateSigningProfileReply {
int32 profile_id = 1;
}
message CreateContactProfileRequest {
/// The name of the new signing profile
string profile_name = 1;
/// The address
string address = 2;
}
message CreateContactProfileReply {
/// The profile id
int32 profile_id = 1;
}
message GetSigningProfilesRequest {
}
@ -82,6 +103,14 @@ message GetSigningProfilesReply {
repeated SqueakProfile squeak_profiles = 1;
}
message GetContactProfilesRequest {
}
message GetContactProfilesReply {
/// The squeak profiles
repeated SqueakProfile squeak_profiles = 1;
}
message GetSqueakProfileRequest {
/// The profile id
int32 profile_id = 1;

View file

@ -30,12 +30,24 @@ class SqueakAdminServerHandler(object):
logger.info("New profile_id: {}".format(profile_id))
return profile_id
def handle_create_contact_profile(self, profile_name, squeak_address):
logger.info("Handle create contact profile with name: {}, address: {}".format(profile_name, squeak_address))
profile_id = self.squeak_node.create_contact_profile(profile_name, squeak_address)
logger.info("New profile_id: {}".format(profile_id))
return profile_id
def handle_get_signing_profiles(self):
logger.info("Handle get signing profiles.")
profiles = self.squeak_node.get_signing_profiles()
logger.info("Got number of profiles: {}".format(len(profiles)))
return profiles
def handle_get_contact_profiles(self):
logger.info("Handle get contact profiles.")
profiles = self.squeak_node.get_contact_profiles()
logger.info("Got number of profiles: {}".format(len(profiles)))
return profiles
def handle_get_squeak_profile(self, profile_id):
logger.info("Handle get squeak profile with id: {}".format(profile_id))
squeak_profile = self.squeak_node.get_squeak_profile(profile_id)

View file

@ -34,6 +34,15 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer):
profile_id = self.handler.handle_create_signing_profile(profile_name)
return squeak_admin_pb2.CreateSigningProfileReply(profile_id=profile_id,)
def CreateContactProfile(self, request, context):
profile_name = request.profile_name
squeak_address = request.address
profile_id = self.handler.handle_create_contact_profile(
profile_name,
squeak_address,
)
return squeak_admin_pb2.CreateContactProfileReply(profile_id=profile_id,)
def GetSigningProfiles(self, request, context):
profiles = self.handler.handle_get_signing_profiles()
profile_msgs = [
@ -45,6 +54,17 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer):
squeak_profiles=profile_msgs
)
def GetContactProfiles(self, request, context):
profiles = self.handler.handle_get_contact_profiles()
profile_msgs = [
self._squeak_profile_to_message(profile)
for profile in
profiles
]
return squeak_admin_pb2.GetContactProfilesReply(
squeak_profiles=profile_msgs
)
def GetSqueakProfile(self, request, context):
profile_id = request.profile_id
squeak_profile = self.handler.handle_get_squeak_profile(profile_id)

View file

@ -0,0 +1,26 @@
import logging
from bitcoin.base58 import Base58ChecksumError
from bitcoin.wallet import CBitcoinAddressError
from squeak.core import CheckSqueak, CheckSqueakError, CSqueak
from squeak.core.signing import CSqueakAddress
logger = logging.getLogger(__name__)
class SqueakAddressValidator(object):
"""Validates addresses
"""
def __init__(self) -> None:
pass
def validate(self, squeak_address_str: str) -> bool:
if not squeak_address_str:
return False
try:
CSqueakAddress(squeak_address_str)
return True
except (Base58ChecksumError, CBitcoinAddressError) as e:
logger.info("Got invalid address with error: {}".format(e))
return False

View file

@ -4,6 +4,7 @@ from squeak.core.encryption import (
)
from squeak.core.signing import CSigningKey, CSqueakAddress
from squeakserver.core.squeak_address_validator import SqueakAddressValidator
from squeakserver.node.squeak_block_periodic_worker import SqueakBlockPeriodicWorker
from squeakserver.node.squeak_block_queue_worker import SqueakBlockQueueWorker
from squeakserver.node.squeak_block_verifier import SqueakBlockVerifier
@ -111,9 +112,26 @@ class SqueakNode:
)
return self.postgres_db.insert_profile(squeak_profile)
def create_contact_profile(self, profile_name, squeak_address):
address_validator = SqueakAddressValidator()
if not address_validator.validate(squeak_address):
raise Exception('Invalid squeak address: {}'.format(squeak_address))
squeak_profile = SqueakProfile(
profile_id=None,
profile_name=profile_name,
private_key=None,
address=squeak_address,
sharing=False,
following=False,
)
return self.postgres_db.insert_profile(squeak_profile)
def get_signing_profiles(self):
return self.postgres_db.get_signing_profiles()
def get_contact_profiles(self):
return self.postgres_db.get_contact_profiles()
def get_squeak_profile(self, profile_id):
return self.postgres_db.get_profile(profile_id)

View file

@ -188,7 +188,7 @@ class PostgresDb:
return row["profile_id"]
def get_signing_profiles(self):
""" Get a profile. """
""" Get all signing profiles. """
sql = """
SELECT * FROM profile
WHERE private_key IS NOT NULL;
@ -199,6 +199,18 @@ class PostgresDb:
profiles = [self._parse_squeak_profile(row) for row in rows]
return profiles
def get_contact_profiles(self):
""" Get all contact profiles. """
sql = """
SELECT * FROM profile
WHERE private_key IS NULL;
"""
with self.get_cursor() as curs:
curs.execute(sql)
rows = curs.fetchall()
profiles = [self._parse_squeak_profile(row) for row in rows]
return profiles
def get_profile(self, profile_id):
""" Get a profile. """
sql = """