diff --git a/.classpath b/.classpath index c079bf4..e536da1 100644 --- a/.classpath +++ b/.classpath @@ -13,9 +13,9 @@ + - @@ -28,13 +28,12 @@ - + - - + + + - - diff --git a/.settings/org.eclipse.wst.common.component b/.settings/org.eclipse.wst.common.component index 73815f6..964827a 100644 --- a/.settings/org.eclipse.wst.common.component +++ b/.settings/org.eclipse.wst.common.component @@ -1,9 +1,11 @@ - - + + - + + + diff --git a/.settings/org.eclipse.wst.common.project.facet.core.xml b/.settings/org.eclipse.wst.common.project.facet.core.xml index 7c7ce15..f1ff4b5 100644 --- a/.settings/org.eclipse.wst.common.project.facet.core.xml +++ b/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -3,6 +3,6 @@ - + diff --git a/.settings/org.eclipse.wst.validation.prefs b/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 0000000..04cad8c --- /dev/null +++ b/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,2 @@ +disabled=06target +eclipse.preferences.version=1 diff --git a/pom.xml b/pom.xml index 358064b..c00c9d7 100644 --- a/pom.xml +++ b/pom.xml @@ -24,18 +24,20 @@ UTF-8 1.8 8.4.4 + 1.23.0 + v4-rev488-1.23.0 + v3-rev271-1.23.0 - org.springframework.boot - spring-boot-starter-jdbc + org.springframework.boot + spring-boot-starter-jdbc - org.springframework.boot spring-boot-starter-data-jpa @@ -67,6 +69,37 @@ spring-security-test test + + org.vaadin.blackbluegl + calendar-component + 2.0-BETA4 + + + + com.google.apis + google-api-services-calendar + ${google-api-calendar-version} + + + com.google.api-client + google-api-client-appengine + ${google-api-version} + + + com.google.api-client + google-api-client-gson + ${google-api-version} + + + com.google.oauth-client + google-oauth-client-java6 + ${google-api-version} + + + com.google.oauth-client + google-oauth-client-jetty + ${google-api-version} + @@ -87,8 +120,34 @@ org.springframework.boot spring-boot-maven-plugin + + + com.vaadin + vaadin-maven-plugin + ${vaadin.version} + + + + update-theme + update-widgetset + compile + + compile-theme + + + + + true + + + + + vaadin-addons + http://maven.vaadin.com/vaadin-addons + + diff --git a/src/main/java/de/kreth/clubhelperbackend/google/GoogleBaseAdapter.java b/src/main/java/de/kreth/clubhelperbackend/google/GoogleBaseAdapter.java new file mode 100644 index 0000000..c46c4d2 --- /dev/null +++ b/src/main/java/de/kreth/clubhelperbackend/google/GoogleBaseAdapter.java @@ -0,0 +1,147 @@ +package de.kreth.clubhelperbackend.google; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.api.client.auth.oauth2.Credential; +import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp; +import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver; +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; +import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.store.FileDataStoreFactory; +import com.google.api.services.calendar.CalendarScopes; + +public abstract class GoogleBaseAdapter { + + private static final int GOOGLE_SECRET_PORT = 59431; + + /** Application name. */ + protected static final String APPLICATION_NAME = "ClubHelperVaadin"; + /** Directory to store user credentials for this application. */ + protected static final File DATA_STORE_DIR = new File( + System.getProperty("catalina.base"), ".credentials"); + /** Global instance of the JSON factory. */ + protected static final JsonFactory JSON_FACTORY = JacksonFactory + .getDefaultInstance(); + /** + * Global instance of the scopes required by this quickstart. + * + * If modifying these scopes, delete your previously saved credentials + */ + static final List SCOPES = Arrays.asList(CalendarScopes.CALENDAR); + + protected static Credential credential; + + protected static final Logger log = LoggerFactory + .getLogger(GoogleBaseAdapter.class); + /** Global instance of the {@link FileDataStoreFactory}. */ + protected final FileDataStoreFactory DATA_STORE_FACTORY; + /** Global instance of the HTTP transport. */ + protected final HttpTransport HTTP_TRANSPORT; + + public GoogleBaseAdapter() throws GeneralSecurityException, IOException { + super(); + HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); + DATA_STORE_FACTORY = new FileDataStoreFactory(DATA_STORE_DIR); + DATA_STORE_DIR.mkdirs(); + } + + protected void checkRefreshToken(String serverName) throws IOException { + + if (credential == null) { + credential = authorize(serverName); + } + + if ((credential.getExpiresInSeconds() != null + && credential.getExpiresInSeconds() < 3600)) { + + if (log.isDebugEnabled()) { + log.debug("Security needs refresh, trying."); + } + boolean result = credential.refreshToken(); + if (log.isDebugEnabled()) { + log.debug("Token refresh " + + (result ? "successfull." : "failed.")); + } + } + } + + /** + * Creates an authorized Credential object. + * + * @param request + * + * @return an authorized Credential object. + * @throws IOException + */ + private synchronized Credential authorize(String serverName) + throws IOException { + if (credential != null && (credential.getExpiresInSeconds() != null + && credential.getExpiresInSeconds() < 3600)) { + credential.refreshToken(); + return credential; + } + // Load client secrets. + InputStream in = getClass().getResourceAsStream("/client_secret.json"); + if (in == null) { + File inHome = new File(System.getProperty("user.home"), + "client_secret.json"); + if (inHome.exists()) { + if (log.isInfoEnabled()) { + log.info( + "Google secret not found as Resource, using user Home file."); + } + in = new FileInputStream(inHome); + } else { + log.error( + "Failed to load client_secret.json. Download from google console."); + return null; + } + } + GoogleClientSecrets clientSecrets = GoogleClientSecrets + .load(JSON_FACTORY, new InputStreamReader(in)); + if (log.isTraceEnabled()) { + log.trace("client secret json resource loaded."); + } + + // Build flow and trigger user authorization request. + GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder( + HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES) + .setDataStoreFactory(DATA_STORE_FACTORY) + .setAccessType("offline").setApprovalPrompt("force") + .build(); + + if (log.isDebugEnabled()) { + log.debug("Configuring google LocalServerReceiver on " + serverName + + ":" + GOOGLE_SECRET_PORT); + } + + LocalServerReceiver localServerReceiver = new LocalServerReceiver.Builder() + .setHost(serverName).setPort(GOOGLE_SECRET_PORT).build(); + + Credential credential = new AuthorizationCodeInstalledApp(flow, + localServerReceiver).authorize("user"); + if (log.isDebugEnabled()) { + log.debug( + "Credentials saved to " + DATA_STORE_DIR.getAbsolutePath()); + } + + credential.setExpiresInSeconds(Long.valueOf(691200L)); + + return credential; + } + +} \ No newline at end of file diff --git a/src/main/java/de/kreth/clubhelperbackend/google/calendar/CalendarAdapter.java b/src/main/java/de/kreth/clubhelperbackend/google/calendar/CalendarAdapter.java new file mode 100644 index 0000000..2102c08 --- /dev/null +++ b/src/main/java/de/kreth/clubhelperbackend/google/calendar/CalendarAdapter.java @@ -0,0 +1,182 @@ +package de.kreth.clubhelperbackend.google.calendar; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import com.google.api.client.util.DateTime; +import com.google.api.services.calendar.model.Calendar; +import com.google.api.services.calendar.model.CalendarList; +import com.google.api.services.calendar.model.CalendarListEntry; +import com.google.api.services.calendar.model.Event; +import com.google.api.services.calendar.model.EventAttendee; + +import de.kreth.clubhelperbackend.google.GoogleBaseAdapter; +import de.kreth.clubhelperbackend.google.calendar.CalendarResource.CalendarKonfig; + +public class CalendarAdapter extends GoogleBaseAdapter { + + com.google.api.services.calendar.Calendar service; + private Lock lock = new ReentrantLock(); + private CalendarResource res; + + public CalendarAdapter() throws GeneralSecurityException, IOException { + super(); + res = new CalendarResource(); + + } + + @Override + protected void checkRefreshToken(String serverName) throws IOException { + try { + if (lock.tryLock(10, TimeUnit.SECONDS)) { + try { + super.checkRefreshToken(serverName); + if (service == null && credential != null) { + service = new com.google.api.services.calendar.Calendar.Builder( + HTTP_TRANSPORT, JSON_FACTORY, credential) + .setApplicationName(APPLICATION_NAME) + .build(); + } + } finally { + lock.unlock(); + } + } + } catch (InterruptedException e) { + if (log.isWarnEnabled()) { + log.warn("Lock interrupted", e); + } + } + if (service == null) { + throw new IllegalStateException( + "Calendar Service not instanciated!"); + } + } + + Calendar getCalendarBySummaryName(List items, + String calendarSummary) throws IOException { + + String calendarId = null; + for (CalendarListEntry e : items) { + if (calendarSummary.equals(e.getSummary())) { + calendarId = e.getId(); + break; + } + } + if (calendarId == null) { + throw new IllegalStateException( + "Calendar " + calendarSummary + " not found!"); + } + Calendar cal = service.calendars().get(calendarId).execute(); + return cal; + } + + public List getAllEvents(String serverName) + throws IOException, InterruptedException { + + final List events = new ArrayList<>(); + + List items = getCalendarList(serverName); + final long oldest = getOldest(); + + List configs = res.getConfigs(); + ExecutorService exec = Executors.newFixedThreadPool(configs.size()); + for (CalendarKonfig c : configs) { + exec.execute(new FetchEventsRunner(items, c.getName(), events, + oldest, c.getColor())); + } + + exec.shutdown(); + try { + exec.awaitTermination(20, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error("Thread terminated - event list may be incomplete.", e); + } + return events; + } + + private long getOldest() { + GregorianCalendar oldestCal = new GregorianCalendar(); + oldestCal.set(java.util.Calendar.DAY_OF_MONTH, 1); + oldestCal.set(java.util.Calendar.HOUR_OF_DAY, 0); + oldestCal.set(java.util.Calendar.MINUTE, 0); + oldestCal.add(java.util.Calendar.MONTH, -1); + oldestCal.add(java.util.Calendar.YEAR, -1); + oldestCal.add(java.util.Calendar.HOUR_OF_DAY, -1); + final long oldest = oldestCal.getTimeInMillis(); + return oldest; + } + + List getCalendarList(String serverName) + throws IOException { + checkRefreshToken(serverName); + CalendarList calendarList; + try { + calendarList = service.calendarList().list().execute(); + } catch (IOException e) { + if (log.isWarnEnabled()) { + log.warn("Error fetching Calendar List, trying token refresh", + e); + } + credential.refreshToken(); + if (log.isInfoEnabled()) { + log.info("Successfully refreshed Google Security Token."); + } + calendarList = service.calendarList().list().execute(); + } + return calendarList.getItems(); + } + + private final class FetchEventsRunner implements Runnable { + private final List events; + private final long oldest; + private String colorClass; + private List items; + private String summary; + + private FetchEventsRunner(List items, String summary, + List events, long oldest, String colorClass) { + this.events = events; + this.oldest = oldest; + this.items = items; + this.summary = summary; + this.colorClass = colorClass; + } + + @Override + public void run() { + + try { + log.debug("Fetching events of calendar \"" + summary + "\""); + Calendar calendar = getCalendarBySummaryName(items, summary); + DateTime timeMin = new DateTime(oldest); + List items = service.events().list(calendar.getId()) + .setTimeMin(timeMin).execute().getItems(); + items.forEach(item -> item.set("colorClass", colorClass)); + events.addAll(items); + log.debug("Added " + items.size() + " Events for \"" + summary + + "\""); + + } catch (IOException e) { + log.error("Unable to fetch Events from " + summary, e); + } + } + } + + static Event createDefaultEvent(String summary) { + Event ev = new Event().setGuestsCanInviteOthers(false) + .setGuestsCanModify(false).setGuestsCanSeeOtherGuests(true) + .setSummary(summary); + List attendees = new ArrayList<>(); + ev.setAttendees(attendees); + return ev; + } + +} diff --git a/src/main/java/de/kreth/clubhelperbackend/google/calendar/CalendarResource.java b/src/main/java/de/kreth/clubhelperbackend/google/calendar/CalendarResource.java new file mode 100644 index 0000000..5789257 --- /dev/null +++ b/src/main/java/de/kreth/clubhelperbackend/google/calendar/CalendarResource.java @@ -0,0 +1,81 @@ +package de.kreth.clubhelperbackend.google.calendar; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.StringTokenizer; + +public class CalendarResource { + + private Map configs; + + public CalendarResource() throws IOException { + configs = new HashMap<>(); + + InputStream resStream = getClass() + .getResourceAsStream("/calendars.properties"); + Properties prop = new Properties(); + prop.load(resStream); + + Enumeration keys = prop.keys(); + String className = getClass().getName(); + String packageName = className.substring(0, className.lastIndexOf('.')); + + while (keys.hasMoreElements()) { + String key = keys.nextElement().toString(); + String name = key.substring(packageName.length()); + String value = prop.getProperty(key); + + StringTokenizer tok = new StringTokenizer(name, "."); + name = tok.nextToken(); + String type = tok.nextToken(); + CalendarKonfig conf; + + if (configs.containsKey(name)) { + conf = configs.get(name); + } else { + conf = new CalendarKonfig(null, null); + configs.put(name, conf); + } + switch (type) { + case "name" : + conf.name = value; + break; + + case "color" : + conf.color = value; + + default : + break; + } + } + } + + public List getConfigs() { + return new ArrayList<>(configs.values()); + } + + public class CalendarKonfig { + private String name; + private String color; + + CalendarKonfig(String name, String color) { + super(); + this.name = name; + this.color = color; + } + + public String getName() { + return name; + } + + public String getColor() { + return color; + } + } +} diff --git a/src/main/java/de/kreth/clubhelperbackend/google/calendar/ClubEvent.java b/src/main/java/de/kreth/clubhelperbackend/google/calendar/ClubEvent.java new file mode 100644 index 0000000..eefd7b0 --- /dev/null +++ b/src/main/java/de/kreth/clubhelperbackend/google/calendar/ClubEvent.java @@ -0,0 +1,83 @@ +package de.kreth.clubhelperbackend.google.calendar; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +import org.vaadin.addon.calendar.item.BasicItem; + +import com.google.api.services.calendar.model.Event; +import com.google.api.services.calendar.model.EventDateTime; + +public class ClubEvent extends BasicItem { + + private static final long serialVersionUID = -3600971939167437577L; + + ClubEvent() { + } + + public static ClubEvent parse(Event ev) { + ClubEvent clubEvent = new ClubEvent(); + clubEvent.setCaption(ev.getSummary()); + clubEvent.setStart(toZoned(parse(ev.getStart()))); + + if (clubEvent.getStart() == null) { + clubEvent.setStart(toZoned(parse(ev.getOriginalStartTime()))); + } + clubEvent.setEnd(toZoned(adjustExcludedEndDate(ev))); + clubEvent.setDescription(ev.getDescription()); + + return clubEvent; + } + + private static ZonedDateTime toZoned(Date parse) { + if (parse != null) { + Instant instant = parse.toInstant(); + return ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()); + } else { + return null; + } + } + + public static Date parse(EventDateTime date) { + if (date != null) { + if (date.getDateTime() != null) { + return new Date(date.getDateTime().getValue()); + } else if (date.getDate() != null) { + return new Date(date.getDate().getValue()); + } + } + return null; + } + + public static Date adjustExcludedEndDate( + com.google.api.services.calendar.model.Event e) { + if (e.isEndTimeUnspecified() == false) { + EventDateTime end = e.getEnd(); + GregorianCalendar calendar = new GregorianCalendar(); + calendar.setTimeInMillis(end.getDate() != null + ? end.getDate().getValue() + : end.getDateTime().getValue()); + if (startIsDateOnly(e)) { + calendar.add(Calendar.DAY_OF_MONTH, -1); + } + return calendar.getTime(); + } + return null; + } + + public static boolean startIsDateOnly( + com.google.api.services.calendar.model.Event e) { + + EventDateTime start = e.getStart(); + if (start == null) { + start = e.getOriginalStartTime(); + } + return (start.getDate() != null || (start.getDateTime() != null + && start.getDateTime().isDateOnly())); + } + +} diff --git a/src/main/java/de/kreth/vaadin/clubhelper/vaadinclubhelper/dao/EventBusiness.java b/src/main/java/de/kreth/vaadin/clubhelper/vaadinclubhelper/dao/EventBusiness.java new file mode 100644 index 0000000..45934a1 --- /dev/null +++ b/src/main/java/de/kreth/vaadin/clubhelper/vaadinclubhelper/dao/EventBusiness.java @@ -0,0 +1,46 @@ +package de.kreth.vaadin.clubhelper.vaadinclubhelper.dao; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.vaadin.server.VaadinRequest; + +import de.kreth.clubhelperbackend.google.calendar.CalendarAdapter; +import de.kreth.clubhelperbackend.google.calendar.ClubEvent; + +public class EventBusiness { + + private final List cache = new ArrayList<>(); + + public List loadEvents(VaadinRequest request) { + if (cache.isEmpty() == false) { + return Collections.unmodifiableList(cache); + } + + cache.clear(); + + try { + + // String remoteHost = request.getRemoteHost(); + String remoteHost = "localhost"; + CalendarAdapter adapter = new CalendarAdapter(); + List events = adapter + .getAllEvents(remoteHost); + + for (com.google.api.services.calendar.model.Event ev : events) { + if ("cancelled".equals(ev.getStatus()) == false) { + cache.add(ClubEvent.parse(ev)); + } else { + System.out.println("Cancelled: " + ev.getSummary()); + } + } + } catch (GeneralSecurityException | IOException + | InterruptedException e) { + e.printStackTrace(); + } + return Collections.unmodifiableList(cache); + } +} diff --git a/src/main/java/de/kreth/vaadin/clubhelper/vaadinclubhelper/ui/MainUi.java b/src/main/java/de/kreth/vaadin/clubhelper/vaadinclubhelper/ui/MainUi.java index 9387409..a57c69b 100644 --- a/src/main/java/de/kreth/vaadin/clubhelper/vaadinclubhelper/ui/MainUi.java +++ b/src/main/java/de/kreth/vaadin/clubhelper/vaadinclubhelper/ui/MainUi.java @@ -1,15 +1,26 @@ package de.kreth.vaadin.clubhelper.vaadinclubhelper.ui; +import java.time.LocalDateTime; +import java.time.Month; +import java.util.Collection; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.springframework.beans.factory.annotation.Autowired; +import org.vaadin.addon.calendar.Calendar; +import org.vaadin.addon.calendar.item.BasicItemProvider; +import org.vaadin.addon.calendar.ui.CalendarComponentEvents; import com.vaadin.server.VaadinRequest; import com.vaadin.spring.annotation.SpringUI; +import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.Label; +import com.vaadin.ui.Notification; import com.vaadin.ui.UI; -import com.vaadin.ui.VerticalLayout; +import de.kreth.clubhelperbackend.google.calendar.ClubEvent; +import de.kreth.vaadin.clubhelper.vaadinclubhelper.dao.EventBusiness; import de.kreth.vaadin.clubhelper.vaadinclubhelper.dao.PersonDao; import de.kreth.vaadin.clubhelper.vaadinclubhelper.data.Person; import de.kreth.vaadin.clubhelper.vaadinclubhelper.ui.components.PersonGrid; @@ -20,10 +31,12 @@ public class MainUi extends UI { private static final long serialVersionUID = 7581634188909841919L; @Autowired PersonDao dao; + private ClubEventProvider dataProvider; @Override protected void init(VaadinRequest request) { - VerticalLayout layout = new VerticalLayout(); + + HorizontalLayout layout = new HorizontalLayout(); layout.addComponent(new Label("Persons found:")); List persons = dao.list(); @@ -31,8 +44,40 @@ public class MainUi extends UI { grid.setItems(persons); grid.setCaption("Person Grid"); - layout.addComponent(grid); + dataProvider = new ClubEventProvider(); + Calendar calendar = new Calendar<>(dataProvider) + .withMonth(Month.from(LocalDateTime.now())); + calendar.setCaption("Events"); + calendar.setHandler(this::onItemClick); + + layout.addComponents(grid, calendar); setContent(layout); + ExecutorService exec = Executors.newSingleThreadExecutor(); + exec.execute(() -> { + + EventBusiness business = new EventBusiness(); + List events = business.loadEvents(request); + dataProvider.setItems(events); + System.out.println("Updated data: " + events); + }); + exec.shutdown(); + } + + private void onItemClick(CalendarComponentEvents.ItemClickEvent event) { + ClubEvent ev = (ClubEvent) event.getCalendarItem(); + Notification.show("Clicked: " + ev); + } + + class ClubEventProvider extends BasicItemProvider { + + private static final long serialVersionUID = -5415397258827236704L; + + @Override + public void setItems(Collection items) { + super.setItems(items); + fireItemSetChanged(); + } + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c469839..473711f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,6 @@ spring.datasource.url=jdbc:mysql://192.168.0.8:3306/clubhelper spring.datasource.username=markus -spring.datasource.password=0773 \ No newline at end of file +spring.datasource.password=0773 +security.ignored=/** +security.basic.enable: false +management.security.enabled: false diff --git a/src/main/resources/calendars.properties b/src/main/resources/calendars.properties new file mode 100644 index 0000000..37c4f93 --- /dev/null +++ b/src/main/resources/calendars.properties @@ -0,0 +1,6 @@ +de.kreth.clubhelperbackend.google.calendar.1.name=mtv_wettkampf +de.kreth.clubhelperbackend.google.calendar.1.color=color1 +de.kreth.clubhelperbackend.google.calendar.2.name=mtv_allgemein +de.kreth.clubhelperbackend.google.calendar.2.color=color2 +de.kreth.clubhelperbackend.google.calendar.3.name=Schulferien +de.kreth.clubhelperbackend.google.calendar.3.color=color3 diff --git a/src/main/resources/client_secret.json b/src/main/resources/client_secret.json new file mode 100644 index 0000000..9144a84 --- /dev/null +++ b/src/main/resources/client_secret.json @@ -0,0 +1,13 @@ +{ + "web": { + "client_id": "18873888282-iptk63468sf4to7ajihqmq1l5ggkq54o.apps.googleusercontent.com", + "project_id": "clubhelper", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_secret": "RxtsmNfj6CmQOX_4enrOl9aM", + "redirect_uris": [ + "http://localhost:59431/Callback" + ] + } +} \ No newline at end of file