Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,11 @@ cloudbuild.yaml
cloudbuild-pr.yaml
./kubernetes
.env*
clean-local-database.sh
run-cloud.sh
.prettierrc
./db
.editorconfig
docker-compose*
./.github
./.next
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ yarn-debug.log*
yarn-error.log*

# local env files
.env
.env.local
.env.development.local
.env.test.local
Expand Down
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
FROM node:alpine AS builder
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY . .
COPY package.json yarn.lock ./
RUN yarn
COPY . .
RUN yarn build

FROM node:alpine
Expand All @@ -16,6 +17,7 @@ EXPOSE 3000

COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/postgraphile.tags.json5 ./postgraphile.tags.json5
RUN npx next telemetry disable

CMD ["node_modules/.bin/next", "start"]
CMD ["node_modules/.bin/next", "start"]
61 changes: 55 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Contributions for the following are very welcome.
- [ ] Edit Links
- [x] Redirect Links
- [x] Auth
- [x] Can be disabled
- [x] Powered by Auth0
- [x] Security
- [x] Row Level Security using Auth0 Roles and Permissions
Expand All @@ -43,6 +44,7 @@ Contributions for the following are very welcome.
- [x] Graph: Usage of last 14 days
- [ ] Link Ownership
- [ ] Link Parameters
- [ ] Link Groups (Folders)
- [ ] Private Links
- [ ] Temporary Links
- [ ] Random Alias
Expand Down Expand Up @@ -103,6 +105,7 @@ export GOOGLE_CLOUD_REGION=<gcp-region>
export CLOUDSQL_INSTANCE_NAME=<cloud-sql-instance-name>
export HOSTNAME=https://go.mydomain.com
export LOGONAME=golinks
export AUHT0_ENABLED=true
export AUTH0_DOMAIN=<auth0-domain>
export AUTH0_AUDIENCE=<auth0-audience>
export AUTH0_COOKIE_DOMAIN=go.mydomain.com
Expand Down Expand Up @@ -136,6 +139,12 @@ kubectl apply -f ./kubernetes/istio/destination-rule.yaml

This app leverages [Auth0](https://auth0.com) as an Identity provider. Auth0 is used to manage users and their permissions to access and modify data in this application.

### Enable Auth0

To enable, make sure you set the `AUTH0_ENABLED` env var as `true`.

In case this is set to `false`, every other environment variable prefixed with `AUTH0_` can be considered optional.

### Configuring Auth0

> In the future, these steps will be automated through the Auth0 Provider for Terraform.
Expand Down Expand Up @@ -193,18 +202,56 @@ GraphQL Type definitions are generated on application startup during development

`graphql-let` then is used to generate type definitions in Typescript for development use.

### Local Database with watch mode:
### Local Database without Auth0 in Watch mode:

For development, we use the official `postgres` docker image. This image comes with a feature that initializes the database when provided a folder with scripts.
When started without a volume setup, the image will execute all the `.sql` scripts in the `./database` folder.
For development, we use the official `postgres` docker image. Migrations need to be ran manually using `dbmate` and the SQL scripts provided.

Start the database:

```sh
docker-compose up -d db
```

Prepare the project and run in development mode:
Run the migrations using [`dbmate`](https://github.com/amacneil/dbmate):

```sh
export DATABASE_URL=postgres://dev:[email protected]:5432/golinks?sslmode=disable
dbmate up
```

Regenerate the `./lib/type-defs.graphqls` with:

```sh
npx postgraphile \
--connection 'postgres://dev:dev@localhost:5432/golinks' \
--schema public \
--export-schema-graphql ./lib/type-defs.graphqls \
--subscriptions \
--dynamic-json \
--no-setof-functions-contain-nulls \
--no-ignore-rbac \
--no-ignore-indexes \
--show-error-stack=json \
--extended-errors hint,detail,errcode \
--append-plugins @graphile-contrib/pg-simplify-inflector \
--enable-query-batching \
--legacy-relations omit \
--no-server
```

Create an `.env.local` file (with auth0 disabled):

```sh
cat > ./.env.local <<EOL
DATABASE_CONNECTION_STRING=postgres://dev:dev@db:5432/golinks
DATABASE_SCHEMA=public
NODE_ENV=development
AUTH0_ENABLED=false
HOSTNAME=http://localhost:3000
LOGONAME=go.mydomain.dev
EOL
```
Download dependencies and run in development mode:

```sh
yarn
Expand All @@ -213,13 +260,14 @@ yarn dev

Access http://localhost:3000 and you should have a live development environment running.

### Locally, with docker and local db:
### Locally, with docker, local db and Auth0:

```sh
cat > ./.env.local <<EOL
DATABASE_CONNECTION_STRING=postgres://dev:dev@db:5432/golinks
DATABASE_SCHEMA=public
NODE_ENV=production
AUTH0_ENABLED=true
AUTH0_DOMAIN=<auth0-domain>
AUTH0_AUDIENCE=<auth0-audience>
AUTH0_CLIENT_ID=<auth0-client-id>
Expand All @@ -237,14 +285,15 @@ docker-compose up

Access http://localhost:3000

### Locally, with docker and cloud sql db:
### Locally, with docker, cloud sql db and Auth0:

```sh
# Environment Variables for the Application
cat > ./.env.cloud <<EOL
DATABASE_CONNECTION_STRING=postgres://<postgraphile-user>:<postgraphile-user-password>@db:5432/golinks
DATABASE_SCHEMA=public
NODE_ENV=production
AUTH0_ENABLED=true
AUTH0_DOMAIN=<auth0-domain>
AUTH0_AUDIENCE=<auth0-audience>
AUTH0_CLIENT_ID=<auth0-client-id>
Expand Down
103 changes: 103 additions & 0 deletions components/LinkCards/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {
Box,
Label,
Text,
Link as FannyLink,
Button,
Set,
Icon,
Stack,
Card,
} from 'bumbag';
import { GetAllLinksQuery } from '../../lib/queries/getAllLinks.graphql';
import { LinkMetricUsageGraph } from '../LinkMetricUsageGraph';

interface Props {
data: GetAllLinksQuery;
isDeleteEnabled: boolean;
isEditEnabled: boolean;
onDelete: (linkId: string) => void | Promise<void>;
onShare: (linkUrl: string) => void | Promise<void>;
onAnalytics: (linkId: string) => void | Promise<void>;
onEdit: (linkId: string) => void | Promise<void>;
isDeleting: boolean;
}

export const LinkCards: React.FC<Props> = ({
data,
isDeleteEnabled,
// isEditEnabled,
onDelete,
// onEdit,
onShare,
// onAnalytics,
isDeleting,
}) => {
const links = data?.links;

if (links?.nodes.length === 0) {
return (
<Box>
<Text>There are no golinks available.</Text>
</Box>
);
}

return (
<Stack direction="column">
{links?.nodes.map((link) => {
return (
<Card
key={link.id}
title={link.alias}
headerAddon={
<Set>
<Button
size="small"
palette="primary"
onClick={() => {
const url = new URL(
link.alias,
document.location.origin
).toString();
onShare(url);
}}>
<Icon icon="solid-share" />
</Button>
{isDeleteEnabled && (
<Button
size="small"
palette="danger"
isLoading={isDeleting}
onClick={() => {
onDelete(link.id);
}}>
<Icon icon="solid-trash-alt" />
</Button>
)}
</Set>
}>
<Stack spacing="major-2">
<Set
orientation="vertical"
spacing="minor-1"
style={{ lineBreak: 'anywhere' }}>
<Label>URL</Label>
<FannyLink href={link.url}>{link.url}</FannyLink>
</Set>

<Set orientation="vertical" spacing="minor-1">
<Label>Month Usage</Label>
<LinkMetricUsageGraph
linkUsageMetrics={link.linkUsageMetrics.nodes}
/>
</Set>
</Stack>
</Card>
);
})}
</Stack>
);
};

export default LinkCards;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Link, FieldStack, Button, InputField } from 'bumbag';
import { Link, InputField } from 'bumbag';
import * as Yup from 'yup';
import { Formik, Form, Field, FormikHelpers } from 'formik';

Expand All @@ -22,39 +22,46 @@ interface Props {
values: Link,
helpers: FormikHelpers<Link>
) => void | Promise<void>;
initialValues?: Link;
}

export const CreateLinkForm: React.FC<Props> = ({ onSubmit }) => {
export const Fields = () => {
return (
<>
<Field
component={InputField.Formik}
name="alias"
label="Alias"
required
/>

<Field
component={InputField.Formik}
name="url"
label="Url"
type="url"
required
/>
</>
);
};

export const FormWrapper: React.FC<Props> = ({
onSubmit,
children,
initialValues = {
alias: '',
url: '',
},
}) => {
return (
<Formik
onSubmit={(values, form) => {
onSubmit(values, form);
}}
validationSchema={CreateLinkSchema}
initialValues={{
alias: '',
url: '',
}}>
<Form>
<FieldStack>
<Field
component={InputField.Formik}
name="alias"
label="Alias"
required
/>

<Field
component={InputField.Formik}
name="url"
label="Url"
type="url"
required
/>

<Button type="submit">Create</Button>
</FieldStack>
</Form>
initialValues={initialValues}>
<Form>{children}</Form>
</Formik>
);
};
Loading