Merge branch 'main' into main
commit
3100adfe36
|
@ -1,2 +1,3 @@
|
|||
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
||||
GOOGLE_API_CREDENTIALS='secret'
|
||||
GOOGLE_API_CREDENTIALS='secret'
|
||||
NEXTAUTH_URL='http://localhost:3000'
|
47
README.md
47
README.md
|
@ -32,6 +32,7 @@ Let's face it: Calendly and other scheduling tools are awesome. It made our live
|
|||
* [Next.js](https://nextjs.org/)
|
||||
* [React](https://reactjs.org/)
|
||||
* [Tailwind](https://tailwindcss.com/)
|
||||
* [Prisma](https://prisma.io/)
|
||||
|
||||
<!-- GETTING STARTED -->
|
||||
## Getting Started
|
||||
|
@ -45,7 +46,7 @@ Here is what you need to be able to run Calendso.
|
|||
* PostgreSQL
|
||||
* Yarn _(recommended)_
|
||||
|
||||
You will also need Google API credentials. You can get this from the [Google API Console](https://console.cloud.google.com/apis/dashboard). Simply create a new project, set the OAuth to use the calendar.events read and write permissions, and then set the callback URL to `<CALENDSO URL>/api/integrations/googlecalendar/callback`.
|
||||
You will also need Google API credentials. You can get this from the [Google API Console](https://console.cloud.google.com/apis/dashboard). More details on this can be found below under the [Obtaining the Google API Credentials section](#Obtaining-the-Google-API-Credentials).
|
||||
|
||||
### Development Setup
|
||||
|
||||
|
@ -57,13 +58,13 @@ You will also need Google API credentials. You can get this from the [Google API
|
|||
```sh
|
||||
yarn install
|
||||
```
|
||||
3. Copy .env.example to .env
|
||||
4. Configure environment variables in the .env file. Replace \<user\>, \<pass\>, \<db-host\>, \<db-port\> with their applicable values
|
||||
3. Copy `.env.example` to `.env`
|
||||
4. Configure environment variables in the .env file. Replace `<user>`, `<pass>`, `<db-host>`, `<db-port>` with their applicable values
|
||||
```
|
||||
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
||||
GOOGLE_API_CREDENTIALS='secret'
|
||||
```
|
||||
5. Set up the database using the Prisma schema
|
||||
5. Set up the database using the Prisma schema (found in `prisma/schema.prisma`)
|
||||
```sh
|
||||
npx prisma db push --preview-feature
|
||||
```
|
||||
|
@ -71,10 +72,14 @@ You will also need Google API credentials. You can get this from the [Google API
|
|||
```sh
|
||||
yarn dev
|
||||
```
|
||||
7. Open the prisma schema (found in `prisma/schema.prisma`) with [Prisma Studio](https://www.prisma.io/studio)
|
||||
8. Click on the user model to allow add a new user record.
|
||||
9. Fill out the fields \(remembering to encrypt your password with [BCrypt](https://bcrypt-generator.com/)\) and click Save 1 Record to create your first user.
|
||||
10. Open a browser to [http://localhost:3000](http://localhost:3000) and login with your first user.
|
||||
7. Open [Prisma Studio](https://www.prisma.io/studio) to look at or modify the database content:
|
||||
```
|
||||
npx prisma studio
|
||||
```
|
||||
9. Click on the `User` model to add a new user record.
|
||||
10. Fill out the fields (remembering to encrypt your password with [BCrypt](https://bcrypt-generator.com/)) and click `Save 1 Record` to create your first user.
|
||||
11. Open a browser to [http://localhost:3000](http://localhost:3000) and login with your just created, first user.
|
||||
|
||||
<!-- ROADMAP -->
|
||||
## Roadmap
|
||||
|
||||
|
@ -85,11 +90,29 @@ See the [open issues](https://github.com/calendso/calendso/issues) for a list of
|
|||
|
||||
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
||||
|
||||
|
||||
1. Fork the project
|
||||
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||
5. Open a pull request
|
||||
3. Make your changes
|
||||
4. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||
5. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||
6. Open a pull request
|
||||
|
||||
|
||||
## Obtaining the Google API Credentials
|
||||
|
||||
1. Open [Google API Console](https://console.cloud.google.com/apis/dashboard). If you don't have a project in your Google Cloud subscription, you'll need to create one before proceeding further. Under Dashboard pane, select Enable APIS and Services.
|
||||
2. In the search box, type calendar and select the Google Calendar API search result.
|
||||
3. Enable the selected API.
|
||||
4. Next, select OAuth consent screen from the side pane. Select the app app type (Internal or External) and enter the basic app details on the first page.
|
||||
5. In the second page on Scopes, select Add or Remove Scopes. Search for Calendar.event and select the scope with scope value `.../auth/calendar.events` and select Update.
|
||||
6. Next, under Test Users, add the Google account(s) you'll using. Make sure the details are correct on the last page of the wizard and your consent screen will be configured.
|
||||
7. Now select Credentials from the side pane and then select Create Credentials. Select the OAuth Client ID option.
|
||||
8. Select Web Application as the Application Type.
|
||||
9. Under Authorized redirect URI's, select Add URI and then add the URI `<CALENDSO URL>/api/integrations/googlecalendar/callback` replacing CALENDSO URL with the URI at which your application runs.
|
||||
10. The key will be created and you will be redirected back to the Credentials page. Select the newly generated client ID under OAuth 2.0 Client IDs.
|
||||
11. Select Download JSON. Copy the contents of this file and paste the entire JSON string in the .env file as the value for GOOGLE_API_CREDENTIALS key.
|
||||
|
||||
|
||||
<!-- LICENSE -->
|
||||
## License
|
||||
|
@ -98,9 +121,11 @@ Distributed under the MIT License. See `LICENSE` for more information.
|
|||
|
||||
<!-- ACKNOWLEDGEMENTS -->
|
||||
## Acknowledgements
|
||||
|
||||
Special thanks to these amazing projects which help power Calendso:
|
||||
* [Next.js](https://nextjs.org/)
|
||||
* [Day.js](https://day.js.org/)
|
||||
* [Tailwind CSS](https://tailwindcss.com/)
|
||||
* [Prisma](https://prisma.io/)
|
||||
|
||||
[product-screenshot]: https://i.imgur.com/4yvFj2E.png
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Calendso API
|
||||
description: The open source Calendly alternative.
|
||||
contact:
|
||||
name: Support
|
||||
email: support@calendso.com
|
||||
license:
|
||||
name: MIT License
|
||||
url: https://opensource.org/licenses/MIT
|
||||
version: 0.1.0
|
||||
server:
|
||||
url: http://localhost:{port}
|
||||
description: Local Development Server
|
||||
variables:
|
||||
port:
|
||||
default: '3000'
|
||||
tags:
|
||||
- name: Authentication
|
||||
description: Auth routes, powered by Next-Auth.js
|
||||
externalDocs:
|
||||
url: http://next-auth.js.org/
|
||||
- name: Availability
|
||||
description: Checking and setting user availability
|
||||
- name: Booking
|
||||
description: Create and manage bookings
|
||||
- name: Integrations
|
||||
description: Manage integrations
|
||||
- name: User
|
||||
description: Manage the user's profile and settings
|
||||
paths:
|
||||
/api/auth/signin:
|
||||
get:
|
||||
description: Displays the sign in page.
|
||||
summary: Displays the sign in page
|
||||
tags:
|
||||
- Authentication
|
||||
/api/auth/signin/:provider:
|
||||
post:
|
||||
description: Starts an OAuth signin flow for the specified provider. The POST submission requires CSRF token from /api/auth/csrf.
|
||||
summary: Starts an OAuth signin flow for the specified provider
|
||||
tags:
|
||||
- Authentication
|
||||
/api/auth/callback/:provider:
|
||||
get:
|
||||
description: Handles returning requests from OAuth services during sign in. For OAuth 2.0 providers that support the state option, the value of the state parameter is checked against the one that was generated when the sign in flow was started - this uses a hash of the CSRF token which MUST match for both the POST and GET calls during sign in.
|
||||
summary: Handles returning requests from OAuth services
|
||||
tags:
|
||||
- Authentication
|
||||
/api/auth/signout:
|
||||
get:
|
||||
description: Displays the sign out page.
|
||||
summary: Displays the sign out page
|
||||
tags:
|
||||
- Authentication
|
||||
post:
|
||||
description: Handles signing out - this is a POST submission to prevent malicious links from triggering signing a user out without their consent. Handles signing out - this is a POST submission to prevent malicious links from triggering signing a user out without their consent.
|
||||
summary: Handles signing out
|
||||
tags:
|
||||
- Authentication
|
||||
/api/auth/session:
|
||||
get:
|
||||
description: Returns client-safe session object - or an empty object if there is no session. The contents of the session object that is returned are configurable with the session callback.
|
||||
summary: Returns client-safe session object
|
||||
tags:
|
||||
- Authentication
|
||||
/api/auth/csrf:
|
||||
get:
|
||||
description: Returns object containing CSRF token. In NextAuth.js, CSRF protection is present on all authentication routes. It uses the "double submit cookie method", which uses a signed HttpOnly, host-only cookie. The CSRF token returned by this endpoint must be passed as form variable named csrfToken in all POST submissions to any API endpoint.
|
||||
summary: Returns object containing CSRF token
|
||||
tags:
|
||||
- Authentication
|
||||
/api/auth/providers:
|
||||
get:
|
||||
description: Returns a list of configured OAuth services and details (e.g. sign in and callback URLs) for each service. It can be used to dynamically generate custom sign up pages and to check what callback URLs are configured for each OAuth provider that is configured.
|
||||
summary: Returns configured OAuth services
|
||||
tags:
|
||||
- Authentication
|
||||
/api/auth/changepw:
|
||||
post:
|
||||
description: Changes the password for the currently logged in account.
|
||||
summary: Changes the password for the currently logged in account
|
||||
tags:
|
||||
- Authentication
|
||||
/api/availability/:user:
|
||||
get:
|
||||
description: Gets the busy times for a particular user, by username.
|
||||
summary: Gets the busy times for a user
|
||||
tags:
|
||||
- Availability
|
||||
/api/availability/day:
|
||||
patch:
|
||||
description: Updates the start and end times for a user's availability.
|
||||
summary: Updates the user's start and end times
|
||||
tags:
|
||||
- Availability
|
||||
/api/availability/eventtype:
|
||||
post:
|
||||
description: Adds a new event type for the user.
|
||||
summary: Adds a new event type
|
||||
tags:
|
||||
- Availability
|
||||
patch:
|
||||
description: Updates an event type for the user.
|
||||
summary: Updates an event type
|
||||
tags:
|
||||
- Availability
|
||||
delete:
|
||||
description: Deletes an event type for the user.
|
||||
summary: Deletes an event type
|
||||
tags:
|
||||
- Availability
|
||||
/api/book/:user:
|
||||
post:
|
||||
description: Creates a booking in the user's calendar.
|
||||
summary: Creates a booking for a user
|
||||
tags:
|
||||
- Booking
|
||||
/api/integrations:
|
||||
get:
|
||||
description: Gets a list of the user's integrations.
|
||||
summary: Gets the user's integrations
|
||||
tags:
|
||||
- Integrations
|
||||
delete:
|
||||
description: Deletes a user's integration
|
||||
summary: Deletes a user's integration
|
||||
tags:
|
||||
- Integrations
|
||||
/api/integrations/googlecalendar/add:
|
||||
get:
|
||||
description: Gets the OAuth URL for a Google Calendar integration.
|
||||
summary: Gets the OAuth URL
|
||||
tags:
|
||||
- Integrations
|
||||
/api/integrations/googlecalendar/callback:
|
||||
post:
|
||||
description: Gets and stores the OAuth token for a Google Calendar integration.
|
||||
summary: Gets and stores the OAuth token
|
||||
tags:
|
||||
- Integrations
|
||||
/api/user/profile:
|
||||
patch:
|
||||
description: Updates a user's profile.
|
||||
summary: Updates a user's profile
|
||||
tags:
|
||||
- User
|
|
@ -7,11 +7,16 @@ export default function Shell(props) {
|
|||
const router = useRouter();
|
||||
const [ session, loading ] = useSession();
|
||||
const [ profileDropdownExpanded, setProfileDropdownExpanded ] = useState(false);
|
||||
const [ mobileMenuExpanded, setMobileMenuExpanded ] = useState(false);
|
||||
|
||||
const toggleProfileDropdown = () => {
|
||||
setProfileDropdownExpanded(!profileDropdownExpanded);
|
||||
}
|
||||
|
||||
const toggleMobileMenu = () => {
|
||||
setMobileMenuExpanded(!mobileMenuExpanded);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-gray-800 pb-32">
|
||||
|
@ -72,27 +77,31 @@ export default function Shell(props) {
|
|||
</div>
|
||||
</div>
|
||||
<div className="-mr-2 flex md:hidden">
|
||||
<button type="button" className="bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white" aria-controls="mobile-menu" aria-expanded="false">
|
||||
<button onClick={toggleMobileMenu} type="button" className="bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white" aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span className="sr-only">Open main menu</span>
|
||||
<svg className="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
{ !mobileMenuExpanded && <svg className="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg className="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
</svg> }
|
||||
{ mobileMenuExpanded && <svg className="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</svg> }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-gray-700 md:hidden" id="mobile-menu">
|
||||
{ mobileMenuExpanded && <div className="border-b border-gray-700 md:hidden" id="mobile-menu">
|
||||
<div className="px-2 py-3 space-y-1 sm:px-3">
|
||||
<a href="#" className="bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium">Dashboard</a>
|
||||
<a href="#" className="text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium">Bookings</a>
|
||||
<a href="#" className="text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium">Availability</a>
|
||||
<a href="#" className="text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium">Integrations</a>
|
||||
<a href="#" className="text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium">Teams</a>
|
||||
<Link href="/">
|
||||
<a className={router.pathname == "/" ? "bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium" : "text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"}>Dashboard</a>
|
||||
</Link>
|
||||
<Link href="/availability">
|
||||
<a className={router.pathname.startsWith("/availability") ? "bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium" : "text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"}>Availability</a>
|
||||
</Link>
|
||||
<Link href="/integrations">
|
||||
<a className={router.pathname.startsWith("/integrations") ? "bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium" : "text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"}>Integrations</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="pt-4 pb-3 border-t border-gray-700">
|
||||
<div className="flex items-center px-5">
|
||||
|
@ -111,12 +120,17 @@ export default function Shell(props) {
|
|||
</button>
|
||||
</div>
|
||||
<div className="mt-3 px-2 space-y-1">
|
||||
<a href="#" className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700">Your Profile</a>
|
||||
<a href="#" className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700">Settings</a>
|
||||
<a href="#" className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700">Sign out</a>
|
||||
<Link href="/settings/profile">
|
||||
<a className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700">Your Profile</a>
|
||||
</Link>
|
||||
<Link href="/settings">
|
||||
<a className={router.pathname.startsWith("/settings") ? "bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium" : "text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"}>Settings</a>
|
||||
</Link>
|
||||
<button onClick={signOut} className="block w-full text-left px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700">Sign out</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</nav>
|
||||
<header className="py-10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
|
|
@ -37,8 +37,8 @@ export default async function handler(req, res) {
|
|||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username: username,
|
||||
email: email,
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword
|
||||
}
|
||||
});
|
||||
|
|
|
@ -33,8 +33,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
username: username,
|
||||
name: name,
|
||||
username,
|
||||
name,
|
||||
bio: description,
|
||||
timeZone: timeZone,
|
||||
},
|
||||
|
|
|
@ -9,10 +9,10 @@ export default function Home(props) {
|
|||
|
||||
if (loading) {
|
||||
return <p className="text-gray-400">Loading...</p>;
|
||||
} else {
|
||||
if (!session) {
|
||||
window.location.href = "/auth/login";
|
||||
}
|
||||
}
|
||||
if (!session) {
|
||||
window.location.href = "/auth/login";
|
||||
return;
|
||||
}
|
||||
|
||||
return(
|
||||
|
|
Loading…
Reference in New Issue