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
27 changes: 27 additions & 0 deletions graphql/mutations/profile.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,30 @@ mutation Logout {
mutation DeleteAccount {
deleteAccount
}

query ApiKeys {
apiKeys {
id
name
prefix
createdAt
lastUsedAt
}
}

mutation CreateApiKey($name: String!) {
createApiKey(name: $name) {
key
apiKey {
id
name
prefix
createdAt
lastUsedAt
}
}
}

mutation RevokeApiKey($id: ID!) {
revokeApiKey(id: $id)
}
27 changes: 25 additions & 2 deletions graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ type AddressResult {
state: State!
}

type ApiKeyResult {
createdAt: DateTime!
id: ID!
lastUsedAt: DateTime
name: String!
prefix: String!
}

enum ArgumentPosition {
NEUTRAL
OPPOSE
Expand Down Expand Up @@ -545,6 +553,12 @@ type CreateUserResult {
id: ID!
}

type CreatedApiKeyResult {
apiKey: ApiKeyResult!
"""The secret is returned only once and cannot be recovered later."""
key: String!
}

"""
Implement the DateTime<Utc> scalar

Expand Down Expand Up @@ -859,6 +873,11 @@ type Mutation {
beginUserRegistration(input: BeginUserRegistrationInput!): LoginResult!
confirmUserEmail(confirmationToken: String!): Boolean!
copyQuestionSubmission(questionSubmissionId: ID!, targetQuestionId: ID!): QuestionSubmissionResult!
"""
Creates an API key for the current registered user.
The secret is returned once and only its hash is stored.
"""
createApiKey(name: String!): CreatedApiKeyResult!
createConversation(input: CreateConversationInput!): ConversationResult!
createUser(input: CreateUserInput!): CreateUserResult!
deleteAccount: ID!
Expand Down Expand Up @@ -898,6 +917,8 @@ type Mutation {
removePoliticianOffice(id: ID!): Boolean!
requestPasswordReset(email: String!): Boolean!
resetPassword(input: ResetPasswordInput!): Boolean!
"""Immediately revokes an API key owned by the current registered user."""
revokeApiKey(id: ID!): Boolean!
setAllCandidateGuideRacesEmailed(candidateGuideId: ID!, wereCandidatesEmailed: Boolean!): Boolean!
updateAddress(address: AddressInput!): AddressResult!
updateArgument(id: ID!, input: UpdateArgumentInput!): ArgumentResult!
Expand Down Expand Up @@ -1336,6 +1357,8 @@ type PublicVotes {

type Query {
allIssueTags: [IssueTagResult!]!
"""Lists active API keys owned by the current registered user."""
apiKeys: [ApiKeyResult!]!
ballotMeasureById(id: ID!): BallotMeasureResult
ballotMeasures(after: String, before: String, filter: BallotMeasureFilter, first: Int, last: Int, sort: BallotMeasureSort): BallotMeasureResultConnection!
billById(id: ID!): BillResult
Expand All @@ -1354,7 +1377,7 @@ type Query {
conversationsByOrganization(limit: Int, organizationId: ID!): [ConversationResult!]!
countiesByState(state: State!): [String!]!
"""
Provides current user based on JWT found in client's access_token cookie
Provides the current user for an authenticated cookie, JWT, or API key.
"""
currentUser: AuthTokenResult
electionById(id: ID!): ElectionResult!
Expand Down Expand Up @@ -2236,4 +2259,4 @@ type VsRating {
ratingText: String!
sigId: JSON!
timespan: JSON!
}
}
37 changes: 32 additions & 5 deletions pages/docs/api/auth.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,50 @@ import { DocsLayout } from "components";
<Divider />
## Authentication

Production API access requires an `Authorization` header containing the bearer
API key supplied during onboarding. This applies to both GraphQL and REST
requests:
Production API access requires an `Authorization` header containing a bearer
API key. The same key works for GraphQL and REST requests:

```json
"Authorization": "Bearer YOUR_API_KEY"
```

If you do not have an API key, you can request one by contacting us at
[info@populist.us](mailto:info@populist.us)
### Create a key

1. [Register for a Populist account](/register), confirm your email, and sign
in.
2. Open [My Profile](/settings/profile) and find **API keys**.
3. Give the key a descriptive name and choose **Create API key**.
4. Copy the secret immediately. Populist stores only its SHA-256 hash, so the
full value cannot be shown again.

An account can have up to 10 active keys. The profile page shows each key's
prefix, creation date, and approximate last-use time. Revoking a key takes
effect immediately and cannot be undone. Create a replacement before revoking
a key that is in production use.

API keys inherit the permissions of their owner. Key-management operations
require an interactive signed-in session; an API key cannot create, list, or
revoke keys.

### Use the key

Store the secret in a server-side environment variable such as
`POPULIST_API_KEY`, then send it with either API style:

```bash
curl https://api.populist.us/api/v1/elections \
--header "Authorization: Bearer $POPULIST_API_KEY"
```

Never put an API key in a browser bundle, URL, query parameter, or client-side
log. Browser applications should call the Populist API through a trusted
backend. Authentication and quota requirements can differ in development or
staging environments; use the base URL and credentials supplied for that
environment.

If a key may have been exposed, revoke it from **My Profile** and replace it in
every deployment that used it.

export default function DocsApiAuth({ children }) {
return <DocsLayout currentPage="api">{children}</DocsLayout>;
}
8 changes: 7 additions & 1 deletion pages/docs/api/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import styles from "components/DocsLayout/DocsLayout.module.scss";
Making a request to the Populist API is easy! To get started quickly, copy this snippet into your terminal and run it:

<CodeBlock
code={`curl 'https://api.populist.us' -H 'Content-Type: application/json' -X POST -d '{"query": "query { elections(filter: { state: CO }) { title description electionDate } }"}'`}
code={`curl 'https://api.populist.us' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-X POST \
-d '{"query": "query { elections(filter: { state: CO }) { title description electionDate } }"}'`}
language="bash"
/>

Expand Down Expand Up @@ -66,6 +70,8 @@ This will return a list of upcoming elections in Colorado. You can modify the `f

Looking for a resource-oriented integration? See the
[REST API quickstart](/docs/api/rest).
Create and manage credentials from [My Profile](/settings/profile); see
[Authentication](/docs/api/auth) for storage, rotation, and revocation guidance.

export default function DocsGuidesQuickStart({ children }) {
return <DocsLayout currentPage="api">{children}</DocsLayout>;
Expand Down
6 changes: 4 additions & 2 deletions pages/docs/api/rest.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@ The production base URL is:

<CodeBlock code={`https://api.populist.us/api/v1`} language="http" />

Use the environment URL and bearer API key supplied during onboarding. All
examples below use production for clarity.
Use the environment URL and a bearer API key created from
[My Profile](/settings/profile). The same key can call GraphQL. All examples
below use production for clarity; see [Authentication](/docs/api/auth) for key
creation, rotation, and revocation.

### Quickstart

Expand Down
78 changes: 78 additions & 0 deletions pages/settings/Profile.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,84 @@
gap: 1.25rem;
}

.apiKeysSection {
align-items: stretch;

> p {
margin: 0;
line-height: 1.5;
}

> form {
display: flex;
flex-direction: column;
gap: 1rem;
}

> a {
color: var(--aqua);
text-align: center;
}
}

.newApiKey {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
border: 1px solid var(--yellow);
border-radius: 0.25rem;
background: rgb(255 255 255 / 8%);

code {
padding: 0.75rem;
overflow-wrap: anywhere;
user-select: all;
background: rgb(0 0 0 / 25%);
}
}

.apiKeyList {
h3 {
margin: 0 0 0.75rem;
}

ul {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin: 0;
padding: 0;
list-style: none;
}

li {
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
padding: 0.75rem 0;
border-top: 1px solid rgb(255 255 255 / 20%);

.apiKeyDetails {
display: flex;
min-width: 0;
flex-direction: column;
gap: 0.35rem;
}

.apiKeyMetadata,
code {
font-size: $text-sm;
}

button {
width: auto;
flex-shrink: 0;
}
}
}

input[type="file"],
input[type="submit"] {
display: none;
Expand Down
Loading
Loading