diff --git a/backend/Dockerfile b/backend/Dockerfile index 9696264..aa5f13d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -5,11 +5,10 @@ COPY . . RUN go get -d ./... RUN go install ./... - FROM alpine:latest RUN apk --no-cache add ca-certificates tzdata WORKDIR /app -COPY --from=builder /go/bin/app . +COPY --from=builder /go/bin/spaceapi.ccc.de ./app COPY config.yaml config.yaml EXPOSE 8080 CMD ["./app"] diff --git a/backend/configFile.go b/backend/configFile.go index 00df543..015e2f8 100644 --- a/backend/configFile.go +++ b/backend/configFile.go @@ -4,4 +4,6 @@ type ConfigFile struct { SharedSecret string `yaml:"shared_secret,omitempty"` MongoDbServer string `yaml:"mongodb_server,omitempty"` MongoDbDatabase string `yaml:"mongodb_database,omitempty"` + DokuWikiUser string `yaml:"doku_wiki_user,omitempty"` + DokuWikiPassword string `yaml:"doku_wiki_password,omitempty"` } diff --git a/backend/database.go b/backend/database.go index 2ae6ef7..30f352c 100644 --- a/backend/database.go +++ b/backend/database.go @@ -53,6 +53,27 @@ func writeCalendar(calendar Calendar) { c.Upsert(bson.M{"space": calendar.Space}, calendar) } +func writeDecentralizedServices(services []DecentrealizedService) { + if len(services) == 0 { + return + } + + session, err := mgo.Dial(config.MongoDbServer) + if err != nil { + panic(err) + } + defer session.Close() + + session.SetMode(mgo.Monotonic, true) + + c := session.DB(config.MongoDbDatabase).C("decentralized_services") + + c.DropCollection() + for _, service := range(services) { + c.Insert(service) + } +} + func updateSpaceurl(spaceUrl SpaceUrl) { session, err := mgo.Dial(config.MongoDbServer) if err != nil { @@ -131,3 +152,20 @@ func readCalendar() []Calendar { return result } + + +func readServices() []DecentrealizedService { + session, err := mgo.Dial(config.MongoDbServer) + if err != nil { + panic(err) + } + defer session.Close() + + session.SetMode(mgo.Monotonic, true) + + c := session.DB(config.MongoDbDatabase).C("decentralized_services") + var result []DecentrealizedService + c.Find(bson.M{}).Iter().All(&result) + + return result +} diff --git a/backend/eva-backend.go b/backend/eva-backend.go index 6d7edcc..69da055 100644 --- a/backend/eva-backend.go +++ b/backend/eva-backend.go @@ -1,11 +1,13 @@ package main import ( + "encoding/csv" "encoding/json" "github.com/gofrs/uuid" "github.com/gorilla/mux" "github.com/robfig/cron" "gopkg.in/yaml.v2" + "io" "io/ioutil" "log" "net/http" @@ -22,11 +24,16 @@ func main() { panic("Can't load config") } config.SharedSecret = os.Getenv("SHARED_SECRET") + config.DokuWikiUser = os.Getenv("DOKU_WIKI_USER") + config.DokuWikiPassword = os.Getenv("DOKU_WIKI_PASSWORD") + + updateDecentralizedServicesList() c := cron.New() - _, err = c.AddFunc("@hourly", func() { + err = c.AddFunc("@hourly", func() { loadSpaceData() getCalendars() + updateDecentralizedServicesList() }) if err != nil { log.Printf("Can't start cron %v", err) @@ -61,6 +68,10 @@ func CalendarIndex(w http.ResponseWriter, r *http.Request) { ReturnJson(w, readCalendar()) } +func DecentralizedServicesIndex(w http.ResponseWriter, r *http.Request) { + ReturnJson(w, readServices()) +} + func SpaceUrlAdd(w http.ResponseWriter, r *http.Request) { spaceUrl := SpaceUrl{} createEntry(&spaceUrl, w, r) @@ -130,3 +141,48 @@ func refreshData(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } + +func updateDecentralizedServicesList() { + client := &http.Client{} + req, err := http.NewRequest("GET", "https://doku.ccc.de/index.php?title=Spezial:Ask&x=-5B-5BKategorie%3ADienste-5D-5D%2F-3FService-2DName%3DName%2F-3FService-2DProduct%3DProdukt%2F-3FService-2DIs-2DCentralized%3DZentralisiert%2F-3FService-2DVisibility%3DSichtbarkeit%2F-3FService-2DOrg%3DOrganisation%2F-3FService-2DType%3DTyp%2F-3FService-2DURL%3DURL%2F-3FService-2DContact%3DKontakt%2F-3FService-2DCheck-23ISO%3DOnline-20Check%2F-3FService-2DState%3DOnline-3F%2F-3FService-2DEncryption%3DVerschl%C3%BCsselung%2F-3FService-2DHas-2DIPv4%3DIPv4%2F-3FService-2DHas-2DIPv6%3DIPv6&mainlabel=-&limit=50&offset=0&format=csv&headers=show&searchlabel=CSV&default=%21no%20result%21&sep=%3B&valuesep=%3B&filename=dienste.csv", nil) + if err != nil{ + log.Fatal(err) + } + req.SetBasicAuth(config.DokuWikiUser, config.DokuWikiPassword) + resp, err := client.Do(req) + if err != nil{ + log.Fatal(err) + } + + r := csv.NewReader(resp.Body) + r.Comma = ';' + + var DecentrealizedServiceList []DecentrealizedService + for { + record, err := r.Read() + if err == io.EOF { + break + } + if err != nil { + log.Fatal(err) + } + + if record[3] == "public" && record[9] == "wahr" && record[6] != "" { + foo := DecentrealizedService{ + record[0], + record[1], + record[6], + } + + DecentrealizedServiceList = append(DecentrealizedServiceList, foo) + } + } + + writeDecentralizedServices(DecentrealizedServiceList) +} + +type DecentrealizedService struct { + Name string `json:"name"` + Type string `json:"type"` + Url string `json:"url"` +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..6bcb5ba --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,15 @@ +module github.com/gidsi/spaceapi.ccc.de + +go 1.13 + +require ( + github.com/PuloV/ics-golang v0.0.0-20190808201353-a3394d3bcade + github.com/channelmeter/iso8601duration v0.0.0-20150204201828-8da3af7a2a61 // indirect + github.com/gidsi/ics-golang v0.0.0-20190331185529-e95e328c4a08 + github.com/gofrs/uuid v3.2.0+incompatible + github.com/gorilla/mux v1.7.3 + github.com/robfig/cron v1.2.0 + github.com/robfig/cron/v3 v3.0.1 + gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 + gopkg.in/yaml.v2 v2.2.8 +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..02cd7e6 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,18 @@ +github.com/PuloV/ics-golang v0.0.0-20190808201353-a3394d3bcade h1:odEkSCl2gLWPtvraEdCyBZbeYyMMTysWPLMurnB8sUY= +github.com/PuloV/ics-golang v0.0.0-20190808201353-a3394d3bcade/go.mod h1:f1P3hjG+t54/IrnXMnnw+gRmFCDR/ryj9xSQ7MPMkQw= +github.com/channelmeter/iso8601duration v0.0.0-20150204201828-8da3af7a2a61 h1:o64h9XF42kVEUuhuer2ehqrlX8rZmvQSU0+Vpj1rF6Q= +github.com/channelmeter/iso8601duration v0.0.0-20150204201828-8da3af7a2a61/go.mod h1:Rp8e0DCtEKwXFOC6JPJQVTz8tuGoGvw6Xfexggh/ed0= +github.com/gidsi/ics-golang v0.0.0-20190331185529-e95e328c4a08 h1:x57vmmjqLdRGv3bZQqoyzt4S08z5pTk/vkWaqXGnswI= +github.com/gidsi/ics-golang v0.0.0-20190331185529-e95e328c4a08/go.mod h1:oIDQdzzcgVLYQRfohyxBaOVr7pyfRydoPcEdVNXry4s= +github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/backend/routes.go b/backend/routes.go index 44d0b5e..f81dbff 100644 --- a/backend/routes.go +++ b/backend/routes.go @@ -36,6 +36,12 @@ var IndexRoutes = Routes{ "/calendar", CalendarIndex, }, + Route{ + "ServicesIndex", + "GET", + "/services", + DecentralizedServicesIndex, + }, Route{ "SpaceUrlAdd", "POST", diff --git a/backend/spaceapi.ccc.de b/backend/spaceapi.ccc.de new file mode 100755 index 0000000..86f60a9 Binary files /dev/null and b/backend/spaceapi.ccc.de differ diff --git a/docker-compose.yml b/docker-compose.yml index 055d709..51e5f37 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,8 @@ services: restart: always environment: - SHARED_SECRET=${SHARED_SECRET:-secret} + - DOKU_WIKI_USER=${DOKU_WIKI_USER} + - DOKU_WIKI_PASSWORD=${DOKU_WIKI_PASSWORD} depends_on: - database database: @@ -34,4 +36,4 @@ networks: ipam: driver: default config: - - subnet: 172.16.238.0/24 \ No newline at end of file + - subnet: 172.16.238.0/24 diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..6356515 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "eva-frontend", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + } + } +} diff --git a/frontend/package.json b/frontend/package.json index 37e0e0f..e572fb6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,26 +3,27 @@ "version": "1.0.0", "private": true, "dependencies": { - "@material-ui/core": "^3.9.3", - "@material-ui/icons": "^3.0.2", - "leaflet": "^1.4.0", + "@material-ui/core": "^4.9.1", + "@material-ui/icons": "^4.9.1", + "classnames": "^2.2.6", + "leaflet": "^1.6.0", "moment": "^2.24.0", "prop-types": "^15.7.2", - "react": "^16.8.6", - "react-dom": "^16.8.6", - "react-leaflet": "^2.2.1", - "react-redux": "^6.0.1", - "react-router-dom": "^5.0.0", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "react-leaflet": "^2.6.1", + "react-redux": "^7.1.3", + "react-router-dom": "^5.1.2", "react-tap-event-plugin": "^3.0.3", - "react-virtualized": "^9.21.0", - "redux": "^4.0.1", + "react-virtualized": "^9.21.2", + "redux": "^4.0.5", "redux-actions": "^2.6.5", "redux-thunk": "^2.3.0", - "rrule": "^2.6.0", - "superagent": "^5.0.2" + "rrule": "^2.6.4", + "superagent": "^5.2.1" }, "devDependencies": { - "react-scripts": "2.1.8" + "react-scripts": "3.3.1" }, "scripts": { "start": "react-scripts start", diff --git a/frontend/src/App.js b/frontend/src/App.js index 0dcb1aa..36713e6 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -8,6 +8,7 @@ import { MuiThemeProvider } from '@material-ui/core/styles'; import theme from './style/theme'; import store from './redux/store'; import IndexContainer from './views/Index'; +import ServicesList from "./views/ServicesList"; import SpaceList from './views/SpaceList'; import UrlListView from './views/UrlListView'; import layout from './layout'; @@ -19,6 +20,7 @@ const App = () => (
)} /> + )} /> )} /> )} />
@@ -27,4 +29,4 @@ const App = () => ( ); -export default App; \ No newline at end of file +export default App; diff --git a/frontend/src/components/Map.jsx b/frontend/src/components/Map.jsx index c0a8b2a..7b19baf 100644 --- a/frontend/src/components/Map.jsx +++ b/frontend/src/components/Map.jsx @@ -33,7 +33,7 @@ class Map extends React.Component { style={{ width: '100vw', height: 'calc(50vh - 60px)', margin: 0, padding: 0, maxWidth: '100%' }} > {this.props.spacedata.items.map( spacedata => ( diff --git a/frontend/src/components/ServiceList.jsx b/frontend/src/components/ServiceList.jsx new file mode 100644 index 0000000..d1a6a6f --- /dev/null +++ b/frontend/src/components/ServiceList.jsx @@ -0,0 +1,100 @@ +import React from 'react'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableRow from '@material-ui/core/TableRow'; +import TableCell from '@material-ui/core/TableCell'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { serviceStruct } from '../redux/modules/services'; +import {withStyles} from "@material-ui/core"; + +const styles = theme => ({ + table: { + fontFamily: theme.typography.fontFamily, + border: 0, + }, + flexContainer: { + display: 'flex', + alignItems: 'center', + boxSizing: 'border-box', + }, + tableRow: {}, + tableRowHover: { + '&:hover': { + backgroundColor: theme.palette.grey[600], + }, + }, + tableRowEven: { + backgroundColor: theme.palette.grey[700], + }, + tableCell: { + flex: 1, + color: '#fff', + border: 0, + }, + noClick: { + cursor: 'initial', + }, +}); + +const formatType = type => { + if (type.indexOf("[") === 0) { + return type.substring(type.indexOf(" ") + 1, type.length - 1); + } + return type; +} + +export class ServiceList extends React.Component { + static propTypes = { + fetchServices: PropTypes.func.isRequired, + services: serviceStruct, + }; + + static defaultProps = { + services: { + items: [], + }, + }; + + componentWillMount() { + this.props.fetchServices(); + } + + render() { + const items = this.props.services.items.sort( + (a, b) => formatType(a.type).toUpperCase().localeCompare(formatType(b.type).toUpperCase()) + ); + return ( +
+ + + {items + .map((service, index) => ( + + + {service.name} + + + {formatType(service.type)} + + + + {service.url} + + + + ) + )} + +
+
+ ); + } +} + +export default withStyles(styles)(ServiceList); diff --git a/frontend/src/components/Toolbar.jsx b/frontend/src/components/Toolbar.jsx index 15cc22a..d4ace10 100644 --- a/frontend/src/components/Toolbar.jsx +++ b/frontend/src/components/Toolbar.jsx @@ -71,6 +71,12 @@ class MyToolbar extends React.Component { > Spaces + window.location.href = '/services'} + containerElement={} + > + Services + window.location.href = 'http://ccc.de/de/imprint'} > diff --git a/frontend/src/containers/ServiceList.jsx b/frontend/src/containers/ServiceList.jsx new file mode 100644 index 0000000..bc91a90 --- /dev/null +++ b/frontend/src/containers/ServiceList.jsx @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { actions as serviceActions } from '../redux/modules/services'; +import Component from '../components/ServiceList'; + +const mapStateToProps = state => ({ + services: state.services, +}); + +const mapDispatchToProps = { + ...serviceActions, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/frontend/src/redux/modules/services.js b/frontend/src/redux/modules/services.js new file mode 100644 index 0000000..efe01cf --- /dev/null +++ b/frontend/src/redux/modules/services.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types'; +import request from 'superagent'; +import { createAction, handleActions } from 'redux-actions'; +import config from '../../api/config'; + +export const serviceElementStruct = PropTypes.shape({ + name: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, +}); +export const itemsStruct = PropTypes.arrayOf(serviceElementStruct); +export const serviceStruct = PropTypes.shape({ + items: itemsStruct, +}); + +const SERVICES_FETCHED = 'SERVICES_FETCHED'; + +export const fetched = createAction(SERVICES_FETCHED, result => result); + +export const fetchServices = () => (dispatch) => { + request + .get(`${config.api.url}/services`) + .set('Content-Type', 'application/json') + .end( + (err, res) => { + if (!err) { + dispatch(fetched(res.body)); + } + } + ); +}; + +export const actions = { + fetchServices, +}; + +export default handleActions({ + [SERVICES_FETCHED]: (state, { payload }) => ( + { + ...state, + items: payload, + } + ), +}, { items: [] }); diff --git a/frontend/src/redux/rootReducer.js b/frontend/src/redux/rootReducer.js index 32b20f2..3054fd9 100644 --- a/frontend/src/redux/rootReducer.js +++ b/frontend/src/redux/rootReducer.js @@ -2,9 +2,11 @@ import { combineReducers } from 'redux'; import spacedataReducer from './modules/spacedata'; import calendarsReducer from './modules/calendar'; import spaceUrlsReducer from './modules/spaceurl'; +import servicesReducer from './modules/services'; export default combineReducers({ spacedata: spacedataReducer, calendars: calendarsReducer, spaceurls: spaceUrlsReducer, + services: servicesReducer, }); diff --git a/frontend/src/views/ServicesList.jsx b/frontend/src/views/ServicesList.jsx new file mode 100644 index 0000000..7900bea --- /dev/null +++ b/frontend/src/views/ServicesList.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ServiceList from '../containers/ServiceList'; + +const ServiceListView = () => ( +
+ +
+); + +export default ServiceListView;