diff --git a/controllers/account.go b/controllers/account.go index d7334839..253a9353 100644 --- a/controllers/account.go +++ b/controllers/account.go @@ -15,10 +15,8 @@ package controllers import ( - "bytes" "encoding/json" "fmt" - "io" "strconv" "github.com/casbin/casdoor/object" @@ -212,67 +210,6 @@ func (c *ApiController) GetAccount() { c.ResponseOk(user, organization) } -// UploadFile -// @Title UploadFile -// @Description upload file -// @Param owner query string true "The owner" -// @Param tag query string true "The tag" -// @Param fullFilePath query string true "The full file path" -// @Param file query string true "The file" -// @Success 200 {object} controllers.Response The Response object -// @router /upload-file [post] -func (c *ApiController) UploadFile() { - userId, ok := c.RequireSignedIn() - if !ok { - return - } - - //owner := c.Input().Get("owner") - tag := c.Input().Get("tag") - parent := c.Input().Get("parent") - fullFilePath := c.Input().Get("fullFilePath") - - file, _, err := c.GetFile("file") - defer file.Close() - if err != nil { - c.ResponseError(err.Error()) - return - } - - fileBuffer := bytes.NewBuffer(nil) - if _, err = io.Copy(fileBuffer, file); err != nil { - c.ResponseError(err.Error()) - return - } - - user := object.GetUser(userId) - application := object.GetApplicationByUser(user) - provider := application.GetStorageProvider() - if provider == nil { - c.ResponseError("No storage provider is found") - return - } - - fileUrl, err := object.UploadFile(provider, fullFilePath, fileBuffer) - if err != nil { - c.ResponseError(err.Error()) - return - } - - switch tag { - case "avatar": - user.Avatar = fileUrl - object.UpdateUser(user.GetId(), user) - case "termsOfUse": - applicationId := fmt.Sprintf("admin/%s", parent) - app := object.GetApplication(applicationId) - app.TermsOfUse = fileUrl - object.UpdateApplication(applicationId, app) - } - - c.ResponseOk(fileUrl) -} - // GetHumanCheck ... func (c *ApiController) GetHumanCheck() { c.Data["json"] = HumanCheck{Type: "none"} diff --git a/controllers/resource.go b/controllers/resource.go new file mode 100644 index 00000000..5f7e16d1 --- /dev/null +++ b/controllers/resource.go @@ -0,0 +1,152 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "path/filepath" + "strings" + + "github.com/casbin/casdoor/object" + "github.com/casbin/casdoor/util" +) + +func (c *ApiController) GetResources() { + owner := c.Input().Get("owner") + + c.Data["json"] = object.GetResources(owner) + c.ServeJSON() +} + +func (c *ApiController) GetResource() { + id := c.Input().Get("id") + + c.Data["json"] = object.GetResource(id) + c.ServeJSON() +} + +func (c *ApiController) UpdateResource() { + id := c.Input().Get("id") + + var resource object.Resource + err := json.Unmarshal(c.Ctx.Input.RequestBody, &resource) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.UpdateResource(id, &resource)) + c.ServeJSON() +} + +func (c *ApiController) AddResource() { + var resource object.Resource + err := json.Unmarshal(c.Ctx.Input.RequestBody, &resource) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.AddResource(&resource)) + c.ServeJSON() +} + +func (c *ApiController) DeleteResource() { + var resource object.Resource + err := json.Unmarshal(c.Ctx.Input.RequestBody, &resource) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.DeleteResource(&resource)) + c.ServeJSON() +} + +func (c *ApiController) UploadResource() { + userId, ok := c.RequireSignedIn() + if !ok { + return + } + + owner := c.Input().Get("owner") + tag := c.Input().Get("tag") + parent := c.Input().Get("parent") + fullFilePath := c.Input().Get("fullFilePath") + + file, header, err := c.GetFile("file") + defer file.Close() + if err != nil { + c.ResponseError(err.Error()) + return + } + + filename := filepath.Base(fullFilePath) + fileBuffer := bytes.NewBuffer(nil) + if _, err = io.Copy(fileBuffer, file); err != nil { + c.ResponseError(err.Error()) + return + } + + user := object.GetUser(userId) + application := object.GetApplicationByUser(user) + provider := application.GetStorageProvider() + if provider == nil { + c.ResponseError("No storage provider is found") + return + } + + fileUrl, objectKey, err := object.UploadFile(provider, fullFilePath, fileBuffer) + if err != nil { + c.ResponseError(err.Error()) + return + } + + fileType := "unknown" + fileFormat := filepath.Ext(fullFilePath) + if strings.Contains(".png|.jpg|.bmp", fileFormat) { + fileType = "image" + } else if strings.Contains(".mp4|.avi", fileFormat) { + fileType = "video" + } + + fileSize := int(header.Size) + resource := &object.Resource{ + Owner: owner, + Name: filename, + CreatedTime: util.GetCurrentTime(), + Tag: tag, + Parent: parent, + FileType: fileType, + FileFormat: fileFormat, + FileSize: fileSize, + Url: fileUrl, + ObjectKey: objectKey, + } + object.AddOrUpdateResource(resource) + + switch tag { + case "avatar": + user.Avatar = fileUrl + object.UpdateUser(user.GetId(), user) + case "termsOfUse": + applicationId := fmt.Sprintf("admin/%s", parent) + app := object.GetApplication(applicationId) + app.TermsOfUse = fileUrl + object.UpdateApplication(applicationId, app) + } + + c.ResponseOk(fileUrl) +} diff --git a/object/adapter.go b/object/adapter.go index 35cb7257..2f62fec7 100644 --- a/object/adapter.go +++ b/object/adapter.go @@ -124,6 +124,11 @@ func (a *Adapter) createTable() { panic(err) } + err = a.Engine.Sync2(new(Resource)) + if err != nil { + panic(err) + } + err = a.Engine.Sync2(new(Token)) if err != nil { panic(err) diff --git a/object/resource.go b/object/resource.go new file mode 100644 index 00000000..9e403f42 --- /dev/null +++ b/object/resource.go @@ -0,0 +1,110 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "fmt" + "xorm.io/core" + + "github.com/casbin/casdoor/util" +) + +type Resource struct { + Owner string `xorm:"varchar(100) notnull pk" json:"owner"` + Name string `xorm:"varchar(100) notnull pk" json:"name"` + CreatedTime string `xorm:"varchar(100)" json:"createdTime"` + + Tag string `xorm:"varchar(100)" json:"tag"` + Parent string `xorm:"varchar(100)" json:"parent"` + FileType string `xorm:"varchar(100)" json:"fileType"` + FileFormat string `xorm:"varchar(100)" json:"fileFormat"` + FileSize int `json:"fileSize"` + Url string `xorm:"varchar(100)" json:"url"` + ObjectKey string `xorm:"varchar(100)" json:"objectKey"` +} + +func GetResources(owner string) []*Resource { + resources := []*Resource{} + err := adapter.Engine.Desc("created_time").Find(&resources, &Resource{Owner: owner}) + if err != nil { + panic(err) + } + + return resources +} + +func getResource(owner string, name string) *Resource { + resource := Resource{Owner: owner, Name: name} + existed, err := adapter.Engine.Get(&resource) + if err != nil { + panic(err) + } + + if existed { + return &resource + } else { + return nil + } +} + +func GetResource(id string) *Resource { + owner, name := util.GetOwnerAndNameFromId(id) + return getResource(owner, name) +} + +func UpdateResource(id string, resource *Resource) bool { + owner, name := util.GetOwnerAndNameFromId(id) + if getResource(owner, name) == nil { + return false + } + + _, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(resource) + if err != nil { + panic(err) + } + + //return affected != 0 + return true +} + +func AddResource(resource *Resource) bool { + affected, err := adapter.Engine.Insert(resource) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func DeleteResource(resource *Resource) bool { + affected, err := adapter.Engine.ID(core.PK{resource.Owner, resource.Name}).Delete(&Resource{}) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func (resource *Resource) GetId() string { + return fmt.Sprintf("%s/%s", resource.Owner, resource.Name) +} + +func AddOrUpdateResource(resource *Resource) bool { + if getResource(resource.Owner, resource.Name) == nil { + return AddResource(resource) + } else { + return UpdateResource(resource.GetId(), resource) + } +} diff --git a/object/storage.go b/object/storage.go index e87a8113..0ba79735 100644 --- a/object/storage.go +++ b/object/storage.go @@ -23,10 +23,10 @@ import ( "github.com/casbin/casdoor/util" ) -func UploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffer) (string, error) { +func UploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffer) (string, string, error) { storageProvider := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, provider.Endpoint) if storageProvider == nil { - return "", fmt.Errorf("the provider type: %s is not supported", provider.Type) + return "", "", fmt.Errorf("the provider type: %s is not supported", provider.Type) } if provider.Domain == "" { @@ -34,10 +34,10 @@ func UploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffe UpdateProvider(provider.GetId(), provider) } - path := util.UrlJoin(util.GetUrlPath(provider.Domain), fullFilePath) - _, err := storageProvider.Put(path, fileBuffer) + objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), fullFilePath) + _, err := storageProvider.Put(objectKey, fileBuffer) if err != nil { - return "", err + return "", "", err } host := "" @@ -52,6 +52,6 @@ func UploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffe host = util.UrlJoin(provider.Domain, "/files") } - fileUrl := fmt.Sprintf("%s?time=%s", util.UrlJoin(host, path), util.GetCurrentUnixTime()) - return fileUrl, nil + fileUrl := fmt.Sprintf("%s?time=%s", util.UrlJoin(host, objectKey), util.GetCurrentUnixTime()) + return fileUrl, objectKey, nil } diff --git a/routers/router.go b/routers/router.go index 54b91f12..d9e335a0 100644 --- a/routers/router.go +++ b/routers/router.go @@ -59,12 +59,13 @@ func initAPI() { beego.Router("/api/update-user", &controllers.ApiController{}, "POST:UpdateUser") beego.Router("/api/add-user", &controllers.ApiController{}, "POST:AddUser") beego.Router("/api/delete-user", &controllers.ApiController{}, "POST:DeleteUser") - beego.Router("/api/upload-file", &controllers.ApiController{}, "POST:UploadFile") + beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword") beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "POST:GetEmailAndPhone") beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode") beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone") beego.Router("/api/get-human-check", &controllers.ApiController{}, "GET:GetHumanCheck") + beego.Router("/api/get-ldap-user", &controllers.ApiController{}, "POST:GetLdapUser") beego.Router("/api/get-ldaps", &controllers.ApiController{}, "POST:GetLdaps") beego.Router("/api/get-ldap", &controllers.ApiController{}, "POST:GetLdap") @@ -87,6 +88,13 @@ func initAPI() { beego.Router("/api/add-application", &controllers.ApiController{}, "POST:AddApplication") beego.Router("/api/delete-application", &controllers.ApiController{}, "POST:DeleteApplication") + beego.Router("/api/get-resources", &controllers.ApiController{}, "GET:GetResources") + beego.Router("/api/get-resource", &controllers.ApiController{}, "GET:GetResource") + beego.Router("/api/update-resource", &controllers.ApiController{}, "POST:UpdateResource") + beego.Router("/api/add-resource", &controllers.ApiController{}, "POST:AddResource") + beego.Router("/api/delete-resource", &controllers.ApiController{}, "POST:DeleteResource") + beego.Router("/api/upload-resource", &controllers.ApiController{}, "POST:UploadResource") + beego.Router("/api/get-tokens", &controllers.ApiController{}, "GET:GetTokens") beego.Router("/api/get-token", &controllers.ApiController{}, "GET:GetToken") beego.Router("/api/update-token", &controllers.ApiController{}, "POST:UpdateToken") diff --git a/web/src/App.js b/web/src/App.js index e2e62e28..376e4ee5 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -27,6 +27,8 @@ import ProviderListPage from "./ProviderListPage"; import ProviderEditPage from "./ProviderEditPage"; import ApplicationListPage from "./ApplicationListPage"; import ApplicationEditPage from "./ApplicationEditPage"; +import ResourceListPage from "./ResourceListPage"; +// import ResourceEditPage from "./ResourceEditPage"; import LdapEditPage from "./LdapEditPage"; import LdapSyncPage from "./LdapSyncPage"; import TokenListPage from "./TokenListPage"; @@ -99,6 +101,8 @@ class App extends Component { this.setState({ selectedMenuKey: '/providers' }); } else if (uri.includes('/applications')) { this.setState({ selectedMenuKey: '/applications' }); + } else if (uri.includes('/resources')) { + this.setState({ selectedMenuKey: '/resources' }); } else if (uri.includes('/tokens')) { this.setState({ selectedMenuKey: '/tokens' }); } else if (uri.includes('/records')) { @@ -317,6 +321,13 @@ class App extends Component { ); + res.push( + + + {i18next.t("general:Resources")} + + + ); res.push( @@ -384,6 +395,8 @@ class App extends Component { this.renderLoginIfNotLoggedIn()}/> this.renderLoginIfNotLoggedIn()}/> this.renderLoginIfNotLoggedIn()}/> + this.renderLoginIfNotLoggedIn()}/> + {/* this.renderLoginIfNotLoggedIn()}/>*/} this.renderLoginIfNotLoggedIn()}/> this.renderLoginIfNotLoggedIn()}/> this.renderLoginIfNotLoggedIn()}/> diff --git a/web/src/ApplicationEditPage.js b/web/src/ApplicationEditPage.js index d503728f..e313821e 100644 --- a/web/src/ApplicationEditPage.js +++ b/web/src/ApplicationEditPage.js @@ -19,7 +19,7 @@ import * as ApplicationBackend from "./backend/ApplicationBackend"; import * as Setting from "./Setting"; import * as ProviderBackend from "./backend/ProviderBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend"; -import * as UserBackend from "./backend/UserBackend"; +import * as ResourceBackend from "./backend/ResourceBackend"; import SignupPage from "./auth/SignupPage"; import LoginPage from "./auth/LoginPage"; import i18next from "i18next"; @@ -99,7 +99,7 @@ class ApplicationEditPage extends React.Component { } this.setState({uploading: true}); const fullFilePath = `termsOfUse/${this.state.application.owner}/${this.state.application.name}.html`; - UserBackend.uploadFile(this.state.application.owner, "termsOfUse", this.state.application.name, fullFilePath, info.file) + ResourceBackend.uploadResource(this.state.application.owner, "termsOfUse", this.state.application.name, fullFilePath, info.file) .then(res => { if (res.status === "ok") { Setting.showMessage("success", i18next.t("application:File uploaded successfully")); diff --git a/web/src/CropperDiv.js b/web/src/CropperDiv.js index 8806fcd7..65e0df92 100644 --- a/web/src/CropperDiv.js +++ b/web/src/CropperDiv.js @@ -18,7 +18,7 @@ import "cropperjs/dist/cropper.css"; import * as Setting from "./Setting"; import {Button, Row, Col, Modal} from 'antd'; import i18next from "i18next"; -import * as UserBackend from "./backend/UserBackend"; +import * as ResourceBackend from "./backend/ResourceBackend"; export const CropperDiv = (props) => { const [image, setImage] = useState(""); @@ -27,6 +27,7 @@ export const CropperDiv = (props) => { const [confirmLoading, setConfirmLoading] = React.useState(false); const {title} = props; const {user} = props; + const {account} = props; const {buttonText} = props; let uploadButton; @@ -57,7 +58,7 @@ export const CropperDiv = (props) => { // Setting.showMessage("success", "uploading..."); const extension = image.substring(image.indexOf('/') + 1, image.indexOf(';base64')); const fullFilePath = `avatar/${user.owner}/${user.name}.${extension}`; - UserBackend.uploadFile("admin", "avatar", "", fullFilePath, blob) + ResourceBackend.uploadResource("admin", "avatar", account.name, fullFilePath, blob) .then((res) => { if (res.status === "ok") { window.location.href = "/account"; diff --git a/web/src/ResourceListPage.js b/web/src/ResourceListPage.js new file mode 100644 index 00000000..7ac5f6ae --- /dev/null +++ b/web/src/ResourceListPage.js @@ -0,0 +1,250 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from "react"; +import {Button, Popconfirm, Table, Upload} from 'antd'; +import {UploadOutlined} from "@ant-design/icons"; +import copy from 'copy-to-clipboard'; +import * as Setting from "./Setting"; +import * as ResourceBackend from "./backend/ResourceBackend"; +import i18next from "i18next"; +import {Link} from "react-router-dom"; + +class ResourceListPage extends React.Component { + constructor(props) { + super(props); + this.state = { + classes: props, + resources: null, + fileList: [], + uploading: false, + }; + } + + UNSAFE_componentWillMount() { + this.getResources(); + } + + getResources() { + ResourceBackend.getResources("admin") + .then((res) => { + this.setState({ + resources: res, + }); + }); + } + + deleteResource(i) { + ResourceBackend.deleteResource(this.state.resources[i]) + .then((res) => { + Setting.showMessage("success", `Resource deleted successfully`); + this.setState({ + resources: Setting.deleteRow(this.state.resources, i), + }); + } + ) + .catch(error => { + Setting.showMessage("error", `Resource failed to delete: ${error}`); + }); + } + + handleUpload(info) { + this.setState({uploading: true}); + const filename = info.fileList[0].name; + const fullFilePath = `resource/${this.props.account.owner}/${this.props.account.name}/${filename}`; + ResourceBackend.uploadResource("admin", "custom", this.props.account.name, fullFilePath, info.file) + .then(res => { + if (res.status === "ok") { + Setting.showMessage("success", i18next.t("application:File uploaded successfully")); + window.location.reload(); + } else { + Setting.showMessage("error", res.msg); + } + }).finally(() => { + this.setState({uploading: false}); + }) + } + + renderUpload() { + return ( + {return false}} onChange={info => {this.handleUpload(info)}}> + + + ) + } + + renderTable(resources) { + const columns = [ + { + title: i18next.t("general:Name"), + dataIndex: 'name', + key: 'name', + width: '150px', + fixed: 'left', + sorter: (a, b) => a.name.localeCompare(b.name), + // render: (text, record, index) => { + // return ( + // + // {text} + // + // ) + // } + }, + { + title: i18next.t("general:Created time"), + dataIndex: 'createdTime', + key: 'createdTime', + width: '160px', + sorter: (a, b) => a.createdTime.localeCompare(b.createdTime), + render: (text, record, index) => { + return Setting.getFormattedDate(text); + } + }, + { + title: i18next.t("resource:Tag"), + dataIndex: 'tag', + key: 'tag', + width: '80px', + sorter: (a, b) => a.tag.localeCompare(b.tag), + }, + { + title: i18next.t("resource:Parent"), + dataIndex: 'parent', + key: 'parent', + width: '80px', + sorter: (a, b) => a.parent.localeCompare(b.parent), + }, + { + title: i18next.t("resource:File type"), + dataIndex: 'fileType', + key: 'fileType', + width: '120px', + sorter: (a, b) => a.fileType.localeCompare(b.fileType), + }, + { + title: i18next.t("resource:File format"), + dataIndex: 'fileFormat', + key: 'fileFormat', + width: '130px', + sorter: (a, b) => a.fileFormat.localeCompare(b.fileFormat), + }, + { + title: i18next.t("resource:File size"), + dataIndex: 'fileSize', + key: 'fileSize', + width: '120px', + sorter: (a, b) => a.fileSize - b.fileSize, + render: (text, record, index) => { + return Setting.getFriendlyFileSize(text); + } + }, + { + title: i18next.t("general:Preview"), + dataIndex: 'preview', + key: 'preview', + width: '100px', + render: (text, record, index) => { + if (record.fileType === "image") { + return ( + + {record.name} + + ) + } else if (record.fileType === "video") { + return ( +
+ +
+ ) + } + } + }, + { + title: i18next.t("general:URL"), + dataIndex: 'url', + key: 'url', + width: '120px', + render: (text, record, index) => { + return ( +
+ +
+ ) + } + }, + { + title: i18next.t("general:Action"), + dataIndex: '', + key: 'op', + width: '70px', + fixed: (Setting.isMobile()) ? "false" : "right", + render: (text, record, index) => { + return ( +
+ {/**/} + this.deleteResource(index)} + okText={i18next.t("user:OK")} + cancelText={i18next.t("user:Cancel")} + > + + +
+ ) + } + }, + ]; + + return ( +
+ ( +
+ {i18next.t("general:Resources")}     + {/**/} + { + this.renderUpload() + } +
+ )} + loading={resources === null} + /> + + ); + } + + render() { + return ( +
+ { + this.renderTable(this.state.resources) + } +
+ ); + } +} + +export default ResourceListPage; diff --git a/web/src/Setting.js b/web/src/Setting.js index 0bc8459e..4d0163c3 100644 --- a/web/src/Setting.js +++ b/web/src/Setting.js @@ -274,6 +274,18 @@ export function getShortText(s, maxLength=35) { } } +export function getFriendlyFileSize(size) { + if (size < 1024) { + return size + ' B'; + } + + let i = Math.floor(Math.log(size) / Math.log(1024)); + let num = (size / Math.pow(1024, i)); + let round = Math.round(num); + num = round < 10 ? num.toFixed(2) : round < 100 ? num.toFixed(1) : round; + return `${num} ${'KMGTPEZY'[i-1]}B`; +} + function getRandomInt(s) { let hash = 0; if (s.length !== 0) { diff --git a/web/src/UserEditPage.js b/web/src/UserEditPage.js index ccdf94d2..1d8ea263 100644 --- a/web/src/UserEditPage.js +++ b/web/src/UserEditPage.js @@ -179,7 +179,7 @@ class UserEditPage extends React.Component { - + diff --git a/web/src/auth/SignupPage.js b/web/src/auth/SignupPage.js index f762e1a5..a36d230c 100644 --- a/web/src/auth/SignupPage.js +++ b/web/src/auth/SignupPage.js @@ -437,7 +437,7 @@ class SignupPage extends React.Component { this.props.history.goBack(); }} > -