diff --git a/config/api_router.py b/config/api_router.py index a53e70c..aab8334 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -1,7 +1,10 @@ from django.conf import settings +from django.urls import include +from django.urls import path from rest_framework.routers import DefaultRouter from rest_framework.routers import SimpleRouter +from democrasite.users.api.views import GitHubLogin from democrasite.users.api.views import UserViewSet from democrasite.webiscite.api.views import BillViewSet @@ -13,4 +16,8 @@ # Unfortunately if we want automatical links for models we can't use a namespace # but I may reconsider anyway # app_name = "api" # noqa: ERA001 -urlpatterns = router.urls +urlpatterns = [ + *router.urls, + path("auth/", include("dj_rest_auth.urls")), + path("auth/github/", GitHubLogin.as_view(), name="github_login"), +] diff --git a/config/settings/base.py b/config/settings/base.py index a9109a5..cd1b53c 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -95,6 +95,8 @@ "rest_framework.authtoken", "corsheaders", "drf_spectacular", + "dj_rest_auth", + "dj_rest_auth.registration", # Machina (forum) dependencies: "mptt", "haystack", @@ -301,25 +303,6 @@ "root": {"level": "INFO", "handlers": ["console"]}, } -# django-debug-toolbar -# ------------------------------------------------------------------------------ -# https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-panels -DEBUG_TOOLBAR_PANELS = [ - "debug_toolbar.panels.history.HistoryPanel", - "debug_toolbar.panels.versions.VersionsPanel", - "debug_toolbar.panels.timer.TimerPanel", - "debug_toolbar.panels.settings.SettingsPanel", - "debug_toolbar.panels.headers.HeadersPanel", - "debug_toolbar.panels.request.RequestPanel", - "debug_toolbar.panels.sql.SQLPanel", - "debug_toolbar.panels.staticfiles.StaticFilesPanel", - "debug_toolbar.panels.templates.TemplatesPanel", - "debug_toolbar.panels.cache.CachePanel", - "debug_toolbar.panels.signals.SignalsPanel", - "debug_toolbar.panels.redirects.RedirectsPanel", - # 'debug_toolbar.panels.profiling.ProfilingPanel', causes errors with frontend -] - # Celery # ------------------------------------------------------------------------------ diff --git a/config/settings/local.py b/config/settings/local.py index 0b79845..14c7aec 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -19,7 +19,7 @@ # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"] # noqa: S104 # Admin site is only enabled during development -INSTALLED_APPS += ["django.contrib.admin"] # type: ignore[used-before-def] +INSTALLED_APPS += ["django.contrib.admin"] # CACHES # ------------------------------------------------------------------------------ diff --git a/config/urls.py b/config/urls.py index 049c2e4..dc63f01 100644 --- a/config/urls.py +++ b/config/urls.py @@ -10,7 +10,6 @@ from django.views.generic import TemplateView from drf_spectacular.views import SpectacularAPIView from drf_spectacular.views import SpectacularSwaggerView -from rest_framework.authtoken.views import obtain_auth_token urlpatterns = [ path( @@ -36,8 +35,6 @@ urlpatterns += [ # API base url path("api/", include("config.api_router")), - # DRF auth token - path("auth-token/", obtain_auth_token), path("api/schema/", SpectacularAPIView.as_view(), name="api-schema"), path( "api/docs/", diff --git a/democrasite-frontend/.env.local b/democrasite-frontend/.env.local deleted file mode 100644 index 3e691a3..0000000 --- a/democrasite-frontend/.env.local +++ /dev/null @@ -1 +0,0 @@ -BASE_API_URL=http://localhost:8000/api diff --git a/democrasite-frontend/.gitignore b/democrasite-frontend/.gitignore index 97b03dd..fd3dbb5 100644 --- a/democrasite-frontend/.gitignore +++ b/democrasite-frontend/.gitignore @@ -26,7 +26,7 @@ yarn-debug.log* yarn-error.log* # local env files -# .env*.local # uncomment if sensitive environment variables added +.env*.local # vercel .vercel diff --git a/democrasite-frontend/api/auto/.openapi-generator/FILES b/democrasite-frontend/api/auto/.openapi-generator/FILES deleted file mode 100644 index 214ee44..0000000 --- a/democrasite-frontend/api/auto/.openapi-generator/FILES +++ /dev/null @@ -1,13 +0,0 @@ -.openapi-generator-ignore -apis/ApiApi.ts -apis/AuthTokenApi.ts -apis/index.ts -index.ts -models/AuthToken.ts -models/Bill.ts -models/PatchedBill.ts -models/PatchedUser.ts -models/PullRequest.ts -models/User.ts -models/index.ts -runtime.ts diff --git a/democrasite-frontend/api/auto/apis/ApiApi.ts b/democrasite-frontend/api/auto/apis/ApiApi.ts deleted file mode 100644 index 429a01f..0000000 --- a/democrasite-frontend/api/auto/apis/ApiApi.ts +++ /dev/null @@ -1,519 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Democrasite API - * Documentation of API endpoints of Democrasite - * - * The version of the OpenAPI document: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -import * as runtime from '../runtime'; -import type { - Bill, - PatchedBill, - PatchedUser, - User, -} from '../models/index'; -import { - BillFromJSON, - BillToJSON, - PatchedBillFromJSON, - PatchedBillToJSON, - PatchedUserFromJSON, - PatchedUserToJSON, - UserFromJSON, - UserToJSON, -} from '../models/index'; - -export interface ApiBillsPartialUpdateRequest { - id: number; - patchedBill?: PatchedBill; -} - -export interface ApiBillsRetrieveRequest { - id: number; -} - -export interface ApiBillsUpdateRequest { - id: number; - bill: Bill; -} - -export interface ApiSchemaRetrieveRequest { - format?: ApiSchemaRetrieveFormatEnum; - lang?: ApiSchemaRetrieveLangEnum; -} - -export interface ApiUsersPartialUpdateRequest { - username: string; - patchedUser?: PatchedUser; -} - -export interface ApiUsersRetrieveRequest { - username: string; -} - -export interface ApiUsersUpdateRequest { - username: string; - user: User; -} - -/** - * - */ -export class ApiApi extends runtime.BaseAPI { - - /** - */ - async apiBillsListRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - if (this.configuration && this.configuration.apiKey) { - headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication - } - - const response = await this.request({ - path: `/api/bills/`, - method: 'GET', - headers: headerParameters, - query: queryParameters, - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(BillFromJSON)); - } - - /** - */ - async apiBillsList(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - const response = await this.apiBillsListRaw(initOverrides); - return await response.value(); - } - - /** - */ - async apiBillsPartialUpdateRaw(requestParameters: ApiBillsPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (requestParameters.id === null || requestParameters.id === undefined) { - throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling apiBillsPartialUpdate.'); - } - - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - headerParameters['Content-Type'] = 'application/json'; - - if (this.configuration && this.configuration.apiKey) { - headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication - } - - const response = await this.request({ - path: `/api/bills/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters.id))), - method: 'PATCH', - headers: headerParameters, - query: queryParameters, - body: PatchedBillToJSON(requestParameters.patchedBill), - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => BillFromJSON(jsonValue)); - } - - /** - */ - async apiBillsPartialUpdate(requestParameters: ApiBillsPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.apiBillsPartialUpdateRaw(requestParameters, initOverrides); - return await response.value(); - } - - /** - */ - async apiBillsRetrieveRaw(requestParameters: ApiBillsRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (requestParameters.id === null || requestParameters.id === undefined) { - throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling apiBillsRetrieve.'); - } - - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - if (this.configuration && this.configuration.apiKey) { - headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication - } - - const response = await this.request({ - path: `/api/bills/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters.id))), - method: 'GET', - headers: headerParameters, - query: queryParameters, - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => BillFromJSON(jsonValue)); - } - - /** - */ - async apiBillsRetrieve(requestParameters: ApiBillsRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.apiBillsRetrieveRaw(requestParameters, initOverrides); - return await response.value(); - } - - /** - */ - async apiBillsUpdateRaw(requestParameters: ApiBillsUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (requestParameters.id === null || requestParameters.id === undefined) { - throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling apiBillsUpdate.'); - } - - if (requestParameters.bill === null || requestParameters.bill === undefined) { - throw new runtime.RequiredError('bill','Required parameter requestParameters.bill was null or undefined when calling apiBillsUpdate.'); - } - - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - headerParameters['Content-Type'] = 'application/json'; - - if (this.configuration && this.configuration.apiKey) { - headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication - } - - const response = await this.request({ - path: `/api/bills/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters.id))), - method: 'PUT', - headers: headerParameters, - query: queryParameters, - body: BillToJSON(requestParameters.bill), - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => BillFromJSON(jsonValue)); - } - - /** - */ - async apiBillsUpdate(requestParameters: ApiBillsUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.apiBillsUpdateRaw(requestParameters, initOverrides); - return await response.value(); - } - - /** - * OpenApi3 schema for this API. Format can be selected via content negotiation. - YAML: application/vnd.oai.openapi - JSON: application/vnd.oai.openapi+json - */ - async apiSchemaRetrieveRaw(requestParameters: ApiSchemaRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - const queryParameters: any = {}; - - if (requestParameters.format !== undefined) { - queryParameters['format'] = requestParameters.format; - } - - if (requestParameters.lang !== undefined) { - queryParameters['lang'] = requestParameters.lang; - } - - const headerParameters: runtime.HTTPHeaders = {}; - - if (this.configuration && this.configuration.apiKey) { - headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication - } - - const response = await this.request({ - path: `/api/schema/`, - method: 'GET', - headers: headerParameters, - query: queryParameters, - }, initOverrides); - - return new runtime.JSONApiResponse(response); - } - - /** - * OpenApi3 schema for this API. Format can be selected via content negotiation. - YAML: application/vnd.oai.openapi - JSON: application/vnd.oai.openapi+json - */ - async apiSchemaRetrieve(requestParameters: ApiSchemaRetrieveRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> { - const response = await this.apiSchemaRetrieveRaw(requestParameters, initOverrides); - return await response.value(); - } - - /** - */ - async apiUsersListRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - if (this.configuration && this.configuration.apiKey) { - headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication - } - - const response = await this.request({ - path: `/api/users/`, - method: 'GET', - headers: headerParameters, - query: queryParameters, - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(UserFromJSON)); - } - - /** - */ - async apiUsersList(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - const response = await this.apiUsersListRaw(initOverrides); - return await response.value(); - } - - /** - */ - async apiUsersMeRetrieveRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - if (this.configuration && this.configuration.apiKey) { - headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication - } - - const response = await this.request({ - path: `/api/users/me/`, - method: 'GET', - headers: headerParameters, - query: queryParameters, - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => UserFromJSON(jsonValue)); - } - - /** - */ - async apiUsersMeRetrieve(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.apiUsersMeRetrieveRaw(initOverrides); - return await response.value(); - } - - /** - */ - async apiUsersPartialUpdateRaw(requestParameters: ApiUsersPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (requestParameters.username === null || requestParameters.username === undefined) { - throw new runtime.RequiredError('username','Required parameter requestParameters.username was null or undefined when calling apiUsersPartialUpdate.'); - } - - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - headerParameters['Content-Type'] = 'application/json'; - - if (this.configuration && this.configuration.apiKey) { - headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication - } - - const response = await this.request({ - path: `/api/users/{username}/`.replace(`{${"username"}}`, encodeURIComponent(String(requestParameters.username))), - method: 'PATCH', - headers: headerParameters, - query: queryParameters, - body: PatchedUserToJSON(requestParameters.patchedUser), - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => UserFromJSON(jsonValue)); - } - - /** - */ - async apiUsersPartialUpdate(requestParameters: ApiUsersPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.apiUsersPartialUpdateRaw(requestParameters, initOverrides); - return await response.value(); - } - - /** - */ - async apiUsersRetrieveRaw(requestParameters: ApiUsersRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (requestParameters.username === null || requestParameters.username === undefined) { - throw new runtime.RequiredError('username','Required parameter requestParameters.username was null or undefined when calling apiUsersRetrieve.'); - } - - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - if (this.configuration && this.configuration.apiKey) { - headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication - } - - const response = await this.request({ - path: `/api/users/{username}/`.replace(`{${"username"}}`, encodeURIComponent(String(requestParameters.username))), - method: 'GET', - headers: headerParameters, - query: queryParameters, - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => UserFromJSON(jsonValue)); - } - - /** - */ - async apiUsersRetrieve(requestParameters: ApiUsersRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.apiUsersRetrieveRaw(requestParameters, initOverrides); - return await response.value(); - } - - /** - */ - async apiUsersUpdateRaw(requestParameters: ApiUsersUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (requestParameters.username === null || requestParameters.username === undefined) { - throw new runtime.RequiredError('username','Required parameter requestParameters.username was null or undefined when calling apiUsersUpdate.'); - } - - if (requestParameters.user === null || requestParameters.user === undefined) { - throw new runtime.RequiredError('user','Required parameter requestParameters.user was null or undefined when calling apiUsersUpdate.'); - } - - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - headerParameters['Content-Type'] = 'application/json'; - - if (this.configuration && this.configuration.apiKey) { - headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication - } - - const response = await this.request({ - path: `/api/users/{username}/`.replace(`{${"username"}}`, encodeURIComponent(String(requestParameters.username))), - method: 'PUT', - headers: headerParameters, - query: queryParameters, - body: UserToJSON(requestParameters.user), - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => UserFromJSON(jsonValue)); - } - - /** - */ - async apiUsersUpdate(requestParameters: ApiUsersUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.apiUsersUpdateRaw(requestParameters, initOverrides); - return await response.value(); - } - -} - -/** - * @export - */ -export const ApiSchemaRetrieveFormatEnum = { - Json: 'json', - Yaml: 'yaml' -} as const; -export type ApiSchemaRetrieveFormatEnum = typeof ApiSchemaRetrieveFormatEnum[keyof typeof ApiSchemaRetrieveFormatEnum]; -/** - * @export - */ -export const ApiSchemaRetrieveLangEnum = { - Af: 'af', - Ar: 'ar', - ArDz: 'ar-dz', - Ast: 'ast', - Az: 'az', - Be: 'be', - Bg: 'bg', - Bn: 'bn', - Br: 'br', - Bs: 'bs', - Ca: 'ca', - Ckb: 'ckb', - Cs: 'cs', - Cy: 'cy', - Da: 'da', - De: 'de', - Dsb: 'dsb', - El: 'el', - En: 'en', - EnAu: 'en-au', - EnGb: 'en-gb', - Eo: 'eo', - Es: 'es', - EsAr: 'es-ar', - EsCo: 'es-co', - EsMx: 'es-mx', - EsNi: 'es-ni', - EsVe: 'es-ve', - Et: 'et', - Eu: 'eu', - Fa: 'fa', - Fi: 'fi', - Fr: 'fr', - Fy: 'fy', - Ga: 'ga', - Gd: 'gd', - Gl: 'gl', - He: 'he', - Hi: 'hi', - Hr: 'hr', - Hsb: 'hsb', - Hu: 'hu', - Hy: 'hy', - Ia: 'ia', - Id: 'id', - Ig: 'ig', - Io: 'io', - Is: 'is', - It: 'it', - Ja: 'ja', - Ka: 'ka', - Kab: 'kab', - Kk: 'kk', - Km: 'km', - Kn: 'kn', - Ko: 'ko', - Ky: 'ky', - Lb: 'lb', - Lt: 'lt', - Lv: 'lv', - Mk: 'mk', - Ml: 'ml', - Mn: 'mn', - Mr: 'mr', - Ms: 'ms', - My: 'my', - Nb: 'nb', - Ne: 'ne', - Nl: 'nl', - Nn: 'nn', - Os: 'os', - Pa: 'pa', - Pl: 'pl', - Pt: 'pt', - PtBr: 'pt-br', - Ro: 'ro', - Ru: 'ru', - Sk: 'sk', - Sl: 'sl', - Sq: 'sq', - Sr: 'sr', - SrLatn: 'sr-latn', - Sv: 'sv', - Sw: 'sw', - Ta: 'ta', - Te: 'te', - Tg: 'tg', - Th: 'th', - Tk: 'tk', - Tr: 'tr', - Tt: 'tt', - Udm: 'udm', - Uk: 'uk', - Ur: 'ur', - Uz: 'uz', - Vi: 'vi', - ZhHans: 'zh-hans', - ZhHant: 'zh-hant' -} as const; -export type ApiSchemaRetrieveLangEnum = typeof ApiSchemaRetrieveLangEnum[keyof typeof ApiSchemaRetrieveLangEnum]; diff --git a/democrasite-frontend/api/auto/apis/AuthTokenApi.ts b/democrasite-frontend/api/auto/apis/AuthTokenApi.ts deleted file mode 100644 index 7b897cc..0000000 --- a/democrasite-frontend/api/auto/apis/AuthTokenApi.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Democrasite API - * Documentation of API endpoints of Democrasite - * - * The version of the OpenAPI document: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -import * as runtime from '../runtime'; -import type { - AuthToken, -} from '../models/index'; -import { - AuthTokenFromJSON, - AuthTokenToJSON, -} from '../models/index'; - -export interface AuthTokenCreateRequest { - username: string; - password: string; - token: string; -} - -/** - * - */ -export class AuthTokenApi extends runtime.BaseAPI { - - /** - */ - async authTokenCreateRaw(requestParameters: AuthTokenCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (requestParameters.username === null || requestParameters.username === undefined) { - throw new runtime.RequiredError('username','Required parameter requestParameters.username was null or undefined when calling authTokenCreate.'); - } - - if (requestParameters.password === null || requestParameters.password === undefined) { - throw new runtime.RequiredError('password','Required parameter requestParameters.password was null or undefined when calling authTokenCreate.'); - } - - if (requestParameters.token === null || requestParameters.token === undefined) { - throw new runtime.RequiredError('token','Required parameter requestParameters.token was null or undefined when calling authTokenCreate.'); - } - - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - if (this.configuration && this.configuration.apiKey) { - headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication - } - - const consumes: runtime.Consume[] = [ - { contentType: 'application/x-www-form-urlencoded' }, - { contentType: 'multipart/form-data' }, - { contentType: 'application/json' }, - ]; - // @ts-ignore: canConsumeForm may be unused - const canConsumeForm = runtime.canConsumeForm(consumes); - - let formParams: { append(param: string, value: any): any }; - let useForm = false; - if (useForm) { - formParams = new FormData(); - } else { - formParams = new URLSearchParams(); - } - - if (requestParameters.username !== undefined) { - formParams.append('username', requestParameters.username as any); - } - - if (requestParameters.password !== undefined) { - formParams.append('password', requestParameters.password as any); - } - - if (requestParameters.token !== undefined) { - formParams.append('token', requestParameters.token as any); - } - - const response = await this.request({ - path: `/auth-token/`, - method: 'POST', - headers: headerParameters, - query: queryParameters, - body: formParams, - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => AuthTokenFromJSON(jsonValue)); - } - - /** - */ - async authTokenCreate(requestParameters: AuthTokenCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.authTokenCreateRaw(requestParameters, initOverrides); - return await response.value(); - } - -} diff --git a/democrasite-frontend/api/auto/apis/index.ts b/democrasite-frontend/api/auto/apis/index.ts deleted file mode 100644 index bec1e8f..0000000 --- a/democrasite-frontend/api/auto/apis/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -export * from './ApiApi'; -export * from './AuthTokenApi'; diff --git a/democrasite-frontend/api/auto/models/index.ts b/democrasite-frontend/api/auto/models/index.ts deleted file mode 100644 index 1303ee0..0000000 --- a/democrasite-frontend/api/auto/models/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -export * from './AuthToken'; -export * from './Bill'; -export * from './PatchedBill'; -export * from './PatchedUser'; -export * from './PullRequest'; -export * from './User'; diff --git a/democrasite-frontend/app/api/auth/github/callback/page.tsx b/democrasite-frontend/app/api/auth/github/callback/page.tsx new file mode 100644 index 0000000..fdc570e --- /dev/null +++ b/democrasite-frontend/app/api/auth/github/callback/page.tsx @@ -0,0 +1,10 @@ +export async function getServerSideProps({ + searchParams, +}: { + searchParams: any; +}) { + await fetch( + "http://localhost:3000/api/auth/github?" + + new URLSearchParams(searchParams).toString() + ); +} diff --git a/democrasite-frontend/app/api/auth/github/route.ts b/democrasite-frontend/app/api/auth/github/route.ts new file mode 100644 index 0000000..fa52e7d --- /dev/null +++ b/democrasite-frontend/app/api/auth/github/route.ts @@ -0,0 +1,41 @@ +"use server"; + +import { cookies } from "next/headers"; +import crypto from "crypto"; +import { redirect } from "next/navigation"; +import api from "@/lib/api"; +import { NextRequest } from "next/server"; +import { revalidatePath } from "next/cache"; + +export async function POST() { + // Length was chosen randomly + const state = crypto.randomBytes(32).toString("hex"); + cookies().set("github-oauth-state", state, { + sameSite: "lax", + httpOnly: true, + secure: process.env.NODE_ENV === "production", + }); + + redirect( + `https://github.com/login/oauth/authorize?client_id=${process.env.GITHUB_CLIENT_ID}&state=${state}&scope=user:email` + ); +} + +export async function GET(request: NextRequest) { + const code = request.nextUrl.searchParams.get("code"); + const state = request.nextUrl.searchParams.get("state"); + const serverState = cookies().get("github-oauth-state"); + cookies().delete("github-oauth-state"); + + if (!code || !state || state !== serverState!.value) { + return redirect("/"); + } + + const token = await api.authApi.authGithubCreate({ + socialLogin: { code: code }, + }); + + await revalidatePath("/", "layout"); + + redirect("/"); +} diff --git a/democrasite-frontend/app/bills/[id]/page.tsx b/democrasite-frontend/app/bills/[id]/page.tsx index 2914bee..394043e 100644 --- a/democrasite-frontend/app/bills/[id]/page.tsx +++ b/democrasite-frontend/app/bills/[id]/page.tsx @@ -1,16 +1,12 @@ import { Container, Center } from "@mantine/core"; -import Bill from "@/components/Bill/Bill"; -import fetchBills, { fetchBill } from "@/lib/fetch_bills"; +import { Bill } from "@/components"; +import api from "@/lib/api"; -export async function generateMetadata({ - params, - searchParams, -}: { - params: { id: number }; - searchParams: URLSearchParams; -}) { - return { title: `${(await fetchBill(params.id)).name}` }; +export async function generateMetadata({ params }: { params: { id: number } }) { + return { + title: `${(await api.billsApi.billsRetrieve({ id: params.id })).name}`, + }; } export default async function BillDetail({ @@ -22,7 +18,7 @@ export default async function BillDetail({
- +
@@ -30,7 +26,7 @@ export default async function BillDetail({ } export async function generateStaticParams() { - const bills = await fetchBills(); + const bills = await api.billsApi.billsList(); return bills.map((bill: any) => ({ id: bill.id.toString(), diff --git a/democrasite-frontend/app/page.tsx b/democrasite-frontend/app/page.tsx index 306080b..4619a63 100644 --- a/democrasite-frontend/app/page.tsx +++ b/democrasite-frontend/app/page.tsx @@ -1,10 +1,14 @@ -import BillList from "@/components/BillList/BillList"; -import fetchBills from "@/lib/fetch_bills"; +import api from "@/lib/api"; +import { BillList } from "@/components"; +import { SignInButton } from "@/components"; export default async function Home() { + // console.log(cookies().getAll()); + // console.log(await api.billsApi.billsList()); return (
- + +
); } diff --git a/democrasite-frontend/components/Bill/Bill.tsx b/democrasite-frontend/components/Bill/Bill.tsx index 7d46f9c..3a942a5 100644 --- a/democrasite-frontend/components/Bill/Bill.tsx +++ b/democrasite-frontend/components/Bill/Bill.tsx @@ -1,3 +1,4 @@ +import { Bill } from "@/lib/auto"; import { Title, Text, @@ -8,13 +9,14 @@ import { Container, } from "@mantine/core"; -export default function Bill({ bill }: any) { +export function Bill({ bill }: { bill: Bill }) { return ( + {bill.userSupports !== null && "⭐️"} - Bill {bill.id}: {bill.name} (PR #{bill.pull_request.number}) + Bill {bill.id}: {bill.name} (PR #{bill.pullRequest.number}) {bill.constitutional && Constitutional Amendment} @@ -26,20 +28,20 @@ export default function Bill({ bill }: any) { {bill.description} - + - +{bill.pull_request.additions} + +{bill.pullRequest.additions} - -{bill.pull_request.deletions} + -{bill.pullRequest.deletions} - Yes: {bill.yes_votes.length} + Yes: {bill.yesVotes.length} - No: {bill.no_votes.length} + No: {bill.noVotes.length} diff --git a/democrasite-frontend/components/BillList/BillList.tsx b/democrasite-frontend/components/BillList/BillList.tsx index 4eafde8..2f6f46f 100644 --- a/democrasite-frontend/components/BillList/BillList.tsx +++ b/democrasite-frontend/components/BillList/BillList.tsx @@ -1,31 +1,7 @@ import { Card, Grid, GridCol } from "@mantine/core"; -import Bill from "../../components/Bill/Bill"; +import { Bill } from "@/components"; -const bills = [ - { - name: "test", - description: "This is a test", - time_created: "2024-02-19T00:05:35.164799-06:00", - author: "http://127.0.0.1:8000/api/users/matthew/", - pull_request: "http://127.0.0.1:8000/api/pull-requests/-1/", - yes_votes: [], - no_votes: ["http://127.0.0.1:8000/api/users/mfosterw/"], - url: "http://127.0.0.1:8000/api/bills/1/", - }, - { - name: "hi", - description: - "This bill has an extremely long-winded description. In fact, the description is so long that it will have to be cut off when being displayed. That makes it a pretty long description in my opinion.", - time_created: "2024-02-19T00:05:35.164799-06:00", - author: "http://127.0.0.1:8000/api/users/matthew/", - pull_request: "http://127.0.0.1:8000/api/pull-requests/-2/", - yes_votes: ["http://127.0.0.1:8000/api/users/mfosterw/"], - no_votes: [], - url: "http://127.0.0.1:8000/api/bills/18/", - }, -]; - -export default function BillList(props: any) { +export function BillList(props: any) { const cards = props.bill_list.map((bill: any) => ( diff --git a/democrasite-frontend/components/SignInButton/SignInButton.tsx b/democrasite-frontend/components/SignInButton/SignInButton.tsx new file mode 100644 index 0000000..3a14d13 --- /dev/null +++ b/democrasite-frontend/components/SignInButton/SignInButton.tsx @@ -0,0 +1,11 @@ +import { Button } from "@mantine/core"; + +export function SignInButton() { + return ( +
+ +
+ ); +} diff --git a/democrasite-frontend/components/index.ts b/democrasite-frontend/components/index.ts new file mode 100644 index 0000000..cb096a0 --- /dev/null +++ b/democrasite-frontend/components/index.ts @@ -0,0 +1,3 @@ +export * from "./Bill/Bill"; +export * from "./BillList/BillList"; +export * from "./SignInButton/SignInButton"; diff --git a/democrasite-frontend/lib/api.ts b/democrasite-frontend/lib/api.ts new file mode 100644 index 0000000..77396b3 --- /dev/null +++ b/democrasite-frontend/lib/api.ts @@ -0,0 +1,62 @@ +import { + AuthApi, + BillsApi, + Configuration, + ConfigurationParameters, + ResponseContext, + UsersApi, +} from "./auto"; + +class Api { + protected params: ConfigurationParameters; + config: Configuration; + authApi: AuthApi = new AuthApi(); + billsApi: BillsApi = new BillsApi(); + usersApi: UsersApi = new UsersApi(); + + constructor() { + this.params = { + basePath: process.env.BASE_API_URL, + // TODO: credentials: "include" is not working + // (see also django-cors settings, which did not immediately fix the issue but are probably necessary) + credentials: "include", + middleware: [{ post: this.propogateSessionMiddleWare.bind(this) }], + }; + this.config = new Configuration(this.params); + + this.authApi = new AuthApi(this.config); + this.billsApi = new BillsApi(this.config); + this.usersApi = new UsersApi(this.config); + } + + setCookie(cookie: string) { + this.params.headers = { + ...this.params.headers, + Cookie: cookie, + }; + this.config.config = new Configuration(this.params); + } + + async propogateSessionMiddleWare({ + url, + response, + }: ResponseContext): Promise { + if ( + response.headers.get("set-cookie") === null || + !/\/api\/auth\/.*/.test(url) + ) { + return response; + } + + response.headers.getSetCookie().forEach((cookie) => { + if (cookie.startsWith("sessionid")) { + this.setCookie(cookie); + } + }); + + return response; + } +} + +const api = new Api(); +export default api; diff --git a/democrasite-frontend/api/auto/.openapi-generator-ignore b/democrasite-frontend/lib/auto/.openapi-generator-ignore similarity index 100% rename from democrasite-frontend/api/auto/.openapi-generator-ignore rename to democrasite-frontend/lib/auto/.openapi-generator-ignore diff --git a/democrasite-frontend/lib/auto/.openapi-generator/FILES b/democrasite-frontend/lib/auto/.openapi-generator/FILES new file mode 100644 index 0000000..71805f7 --- /dev/null +++ b/democrasite-frontend/lib/auto/.openapi-generator/FILES @@ -0,0 +1,22 @@ +apis/AuthApi.ts +apis/BillsApi.ts +apis/SchemaApi.ts +apis/UsersApi.ts +apis/index.ts +index.ts +models/Bill.ts +models/Login.ts +models/PasswordChange.ts +models/PasswordReset.ts +models/PasswordResetConfirm.ts +models/PatchedBill.ts +models/PatchedUser.ts +models/PatchedUserDetails.ts +models/PullRequest.ts +models/RestAuthDetail.ts +models/SocialLogin.ts +models/Token.ts +models/User.ts +models/UserDetails.ts +models/index.ts +runtime.ts diff --git a/democrasite-frontend/api/auto/.openapi-generator/VERSION b/democrasite-frontend/lib/auto/.openapi-generator/VERSION similarity index 100% rename from democrasite-frontend/api/auto/.openapi-generator/VERSION rename to democrasite-frontend/lib/auto/.openapi-generator/VERSION diff --git a/democrasite-frontend/lib/auto/apis/AuthApi.ts b/democrasite-frontend/lib/auto/apis/AuthApi.ts new file mode 100644 index 0000000..7877fa4 --- /dev/null +++ b/democrasite-frontend/lib/auto/apis/AuthApi.ts @@ -0,0 +1,395 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Democrasite API + * Documentation of API endpoints of Democrasite + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import * as runtime from '../runtime'; +import type { + Login, + PasswordChange, + PasswordReset, + PasswordResetConfirm, + PatchedUserDetails, + RestAuthDetail, + SocialLogin, + Token, + UserDetails, +} from '../models/index'; +import { + LoginFromJSON, + LoginToJSON, + PasswordChangeFromJSON, + PasswordChangeToJSON, + PasswordResetFromJSON, + PasswordResetToJSON, + PasswordResetConfirmFromJSON, + PasswordResetConfirmToJSON, + PatchedUserDetailsFromJSON, + PatchedUserDetailsToJSON, + RestAuthDetailFromJSON, + RestAuthDetailToJSON, + SocialLoginFromJSON, + SocialLoginToJSON, + TokenFromJSON, + TokenToJSON, + UserDetailsFromJSON, + UserDetailsToJSON, +} from '../models/index'; + +export interface AuthGithubCreateRequest { + socialLogin?: SocialLogin; +} + +export interface AuthLoginCreateRequest { + login: Login; +} + +export interface AuthPasswordChangeCreateRequest { + passwordChange: PasswordChange; +} + +export interface AuthPasswordResetConfirmCreateRequest { + passwordResetConfirm: PasswordResetConfirm; +} + +export interface AuthPasswordResetCreateRequest { + passwordReset: PasswordReset; +} + +export interface AuthUserPartialUpdateRequest { + patchedUserDetails?: PatchedUserDetails; +} + +export interface AuthUserUpdateRequest { + userDetails: UserDetails; +} + +/** + * + */ +export class AuthApi extends runtime.BaseAPI { + + /** + * Login with GitHub using OAuth2 + * Login with GitHub + */ + async authGithubCreateRaw(requestParameters: AuthGithubCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/auth/github/`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: SocialLoginToJSON(requestParameters.socialLogin), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => TokenFromJSON(jsonValue)); + } + + /** + * Login with GitHub using OAuth2 + * Login with GitHub + */ + async authGithubCreate(requestParameters: AuthGithubCreateRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.authGithubCreateRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Check the credentials and return the REST Token if the credentials are valid and authenticated. Calls Django Auth login method to register User ID in Django session framework Accept the following POST parameters: username, password Return the REST Framework Token Object\'s key. + */ + async authLoginCreateRaw(requestParameters: AuthLoginCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.login === null || requestParameters.login === undefined) { + throw new runtime.RequiredError('login','Required parameter requestParameters.login was null or undefined when calling authLoginCreate.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/auth/login/`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: LoginToJSON(requestParameters.login), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => TokenFromJSON(jsonValue)); + } + + /** + * Check the credentials and return the REST Token if the credentials are valid and authenticated. Calls Django Auth login method to register User ID in Django session framework Accept the following POST parameters: username, password Return the REST Framework Token Object\'s key. + */ + async authLoginCreate(requestParameters: AuthLoginCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.authLoginCreateRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Calls Django logout method and delete the Token object assigned to the current User object. Accepts/Returns nothing. + */ + async authLogoutCreateRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/auth/logout/`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => RestAuthDetailFromJSON(jsonValue)); + } + + /** + * Calls Django logout method and delete the Token object assigned to the current User object. Accepts/Returns nothing. + */ + async authLogoutCreate(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.authLogoutCreateRaw(initOverrides); + return await response.value(); + } + + /** + * Calls Django Auth SetPasswordForm save method. Accepts the following POST parameters: new_password1, new_password2 Returns the success/fail message. + */ + async authPasswordChangeCreateRaw(requestParameters: AuthPasswordChangeCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.passwordChange === null || requestParameters.passwordChange === undefined) { + throw new runtime.RequiredError('passwordChange','Required parameter requestParameters.passwordChange was null or undefined when calling authPasswordChangeCreate.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/auth/password/change/`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: PasswordChangeToJSON(requestParameters.passwordChange), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => RestAuthDetailFromJSON(jsonValue)); + } + + /** + * Calls Django Auth SetPasswordForm save method. Accepts the following POST parameters: new_password1, new_password2 Returns the success/fail message. + */ + async authPasswordChangeCreate(requestParameters: AuthPasswordChangeCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.authPasswordChangeCreateRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Password reset e-mail link is confirmed, therefore this resets the user\'s password. Accepts the following POST parameters: token, uid, new_password1, new_password2 Returns the success/fail message. + */ + async authPasswordResetConfirmCreateRaw(requestParameters: AuthPasswordResetConfirmCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.passwordResetConfirm === null || requestParameters.passwordResetConfirm === undefined) { + throw new runtime.RequiredError('passwordResetConfirm','Required parameter requestParameters.passwordResetConfirm was null or undefined when calling authPasswordResetConfirmCreate.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/auth/password/reset/confirm/`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: PasswordResetConfirmToJSON(requestParameters.passwordResetConfirm), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => RestAuthDetailFromJSON(jsonValue)); + } + + /** + * Password reset e-mail link is confirmed, therefore this resets the user\'s password. Accepts the following POST parameters: token, uid, new_password1, new_password2 Returns the success/fail message. + */ + async authPasswordResetConfirmCreate(requestParameters: AuthPasswordResetConfirmCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.authPasswordResetConfirmCreateRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Calls Django Auth PasswordResetForm save method. Accepts the following POST parameters: email Returns the success/fail message. + */ + async authPasswordResetCreateRaw(requestParameters: AuthPasswordResetCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.passwordReset === null || requestParameters.passwordReset === undefined) { + throw new runtime.RequiredError('passwordReset','Required parameter requestParameters.passwordReset was null or undefined when calling authPasswordResetCreate.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/auth/password/reset/`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: PasswordResetToJSON(requestParameters.passwordReset), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => RestAuthDetailFromJSON(jsonValue)); + } + + /** + * Calls Django Auth PasswordResetForm save method. Accepts the following POST parameters: email Returns the success/fail message. + */ + async authPasswordResetCreate(requestParameters: AuthPasswordResetCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.authPasswordResetCreateRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Reads and updates UserModel fields Accepts GET, PUT, PATCH methods. Default accepted fields: username, first_name, last_name Default display fields: pk, username, email, first_name, last_name Read-only fields: pk, email Returns UserModel fields. + */ + async authUserPartialUpdateRaw(requestParameters: AuthUserPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/auth/user/`, + method: 'PATCH', + headers: headerParameters, + query: queryParameters, + body: PatchedUserDetailsToJSON(requestParameters.patchedUserDetails), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => UserDetailsFromJSON(jsonValue)); + } + + /** + * Reads and updates UserModel fields Accepts GET, PUT, PATCH methods. Default accepted fields: username, first_name, last_name Default display fields: pk, username, email, first_name, last_name Read-only fields: pk, email Returns UserModel fields. + */ + async authUserPartialUpdate(requestParameters: AuthUserPartialUpdateRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.authUserPartialUpdateRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Reads and updates UserModel fields Accepts GET, PUT, PATCH methods. Default accepted fields: username, first_name, last_name Default display fields: pk, username, email, first_name, last_name Read-only fields: pk, email Returns UserModel fields. + */ + async authUserRetrieveRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/auth/user/`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => UserDetailsFromJSON(jsonValue)); + } + + /** + * Reads and updates UserModel fields Accepts GET, PUT, PATCH methods. Default accepted fields: username, first_name, last_name Default display fields: pk, username, email, first_name, last_name Read-only fields: pk, email Returns UserModel fields. + */ + async authUserRetrieve(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.authUserRetrieveRaw(initOverrides); + return await response.value(); + } + + /** + * Reads and updates UserModel fields Accepts GET, PUT, PATCH methods. Default accepted fields: username, first_name, last_name Default display fields: pk, username, email, first_name, last_name Read-only fields: pk, email Returns UserModel fields. + */ + async authUserUpdateRaw(requestParameters: AuthUserUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.userDetails === null || requestParameters.userDetails === undefined) { + throw new runtime.RequiredError('userDetails','Required parameter requestParameters.userDetails was null or undefined when calling authUserUpdate.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/auth/user/`, + method: 'PUT', + headers: headerParameters, + query: queryParameters, + body: UserDetailsToJSON(requestParameters.userDetails), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => UserDetailsFromJSON(jsonValue)); + } + + /** + * Reads and updates UserModel fields Accepts GET, PUT, PATCH methods. Default accepted fields: username, first_name, last_name Default display fields: pk, username, email, first_name, last_name Read-only fields: pk, email Returns UserModel fields. + */ + async authUserUpdate(requestParameters: AuthUserUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.authUserUpdateRaw(requestParameters, initOverrides); + return await response.value(); + } + +} diff --git a/democrasite-frontend/lib/auto/apis/BillsApi.ts b/democrasite-frontend/lib/auto/apis/BillsApi.ts new file mode 100644 index 0000000..2f3d867 --- /dev/null +++ b/democrasite-frontend/lib/auto/apis/BillsApi.ts @@ -0,0 +1,181 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Democrasite API + * Documentation of API endpoints of Democrasite + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import * as runtime from '../runtime'; +import type { + Bill, + PatchedBill, +} from '../models/index'; +import { + BillFromJSON, + BillToJSON, + PatchedBillFromJSON, + PatchedBillToJSON, +} from '../models/index'; + +export interface BillsPartialUpdateRequest { + id: number; + patchedBill?: PatchedBill; +} + +export interface BillsRetrieveRequest { + id: number; +} + +export interface BillsUpdateRequest { + id: number; + bill: Bill; +} + +/** + * + */ +export class BillsApi extends runtime.BaseAPI { + + /** + */ + async billsListRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/bills/`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(BillFromJSON)); + } + + /** + */ + async billsList(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const response = await this.billsListRaw(initOverrides); + return await response.value(); + } + + /** + */ + async billsPartialUpdateRaw(requestParameters: BillsPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.id === null || requestParameters.id === undefined) { + throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling billsPartialUpdate.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/bills/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters.id))), + method: 'PATCH', + headers: headerParameters, + query: queryParameters, + body: PatchedBillToJSON(requestParameters.patchedBill), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => BillFromJSON(jsonValue)); + } + + /** + */ + async billsPartialUpdate(requestParameters: BillsPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.billsPartialUpdateRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + */ + async billsRetrieveRaw(requestParameters: BillsRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.id === null || requestParameters.id === undefined) { + throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling billsRetrieve.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/bills/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters.id))), + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => BillFromJSON(jsonValue)); + } + + /** + */ + async billsRetrieve(requestParameters: BillsRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.billsRetrieveRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + */ + async billsUpdateRaw(requestParameters: BillsUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.id === null || requestParameters.id === undefined) { + throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling billsUpdate.'); + } + + if (requestParameters.bill === null || requestParameters.bill === undefined) { + throw new runtime.RequiredError('bill','Required parameter requestParameters.bill was null or undefined when calling billsUpdate.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/bills/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters.id))), + method: 'PUT', + headers: headerParameters, + query: queryParameters, + body: BillToJSON(requestParameters.bill), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => BillFromJSON(jsonValue)); + } + + /** + */ + async billsUpdate(requestParameters: BillsUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.billsUpdateRaw(requestParameters, initOverrides); + return await response.value(); + } + +} diff --git a/democrasite-frontend/lib/auto/apis/SchemaApi.ts b/democrasite-frontend/lib/auto/apis/SchemaApi.ts new file mode 100644 index 0000000..e90369f --- /dev/null +++ b/democrasite-frontend/lib/auto/apis/SchemaApi.ts @@ -0,0 +1,179 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Democrasite API + * Documentation of API endpoints of Democrasite + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import * as runtime from '../runtime'; + +export interface SchemaRetrieveRequest { + format?: SchemaRetrieveFormatEnum; + lang?: SchemaRetrieveLangEnum; +} + +/** + * + */ +export class SchemaApi extends runtime.BaseAPI { + + /** + * OpenApi3 schema for this API. Format can be selected via content negotiation. - YAML: application/vnd.oai.openapi - JSON: application/vnd.oai.openapi+json + */ + async schemaRetrieveRaw(requestParameters: SchemaRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + if (requestParameters.format !== undefined) { + queryParameters['format'] = requestParameters.format; + } + + if (requestParameters.lang !== undefined) { + queryParameters['lang'] = requestParameters.lang; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/schema/`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * OpenApi3 schema for this API. Format can be selected via content negotiation. - YAML: application/vnd.oai.openapi - JSON: application/vnd.oai.openapi+json + */ + async schemaRetrieve(requestParameters: SchemaRetrieveRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> { + const response = await this.schemaRetrieveRaw(requestParameters, initOverrides); + return await response.value(); + } + +} + +/** + * @export + */ +export const SchemaRetrieveFormatEnum = { + Json: 'json', + Yaml: 'yaml' +} as const; +export type SchemaRetrieveFormatEnum = typeof SchemaRetrieveFormatEnum[keyof typeof SchemaRetrieveFormatEnum]; +/** + * @export + */ +export const SchemaRetrieveLangEnum = { + Af: 'af', + Ar: 'ar', + ArDz: 'ar-dz', + Ast: 'ast', + Az: 'az', + Be: 'be', + Bg: 'bg', + Bn: 'bn', + Br: 'br', + Bs: 'bs', + Ca: 'ca', + Ckb: 'ckb', + Cs: 'cs', + Cy: 'cy', + Da: 'da', + De: 'de', + Dsb: 'dsb', + El: 'el', + En: 'en', + EnAu: 'en-au', + EnGb: 'en-gb', + Eo: 'eo', + Es: 'es', + EsAr: 'es-ar', + EsCo: 'es-co', + EsMx: 'es-mx', + EsNi: 'es-ni', + EsVe: 'es-ve', + Et: 'et', + Eu: 'eu', + Fa: 'fa', + Fi: 'fi', + Fr: 'fr', + Fy: 'fy', + Ga: 'ga', + Gd: 'gd', + Gl: 'gl', + He: 'he', + Hi: 'hi', + Hr: 'hr', + Hsb: 'hsb', + Hu: 'hu', + Hy: 'hy', + Ia: 'ia', + Id: 'id', + Ig: 'ig', + Io: 'io', + Is: 'is', + It: 'it', + Ja: 'ja', + Ka: 'ka', + Kab: 'kab', + Kk: 'kk', + Km: 'km', + Kn: 'kn', + Ko: 'ko', + Ky: 'ky', + Lb: 'lb', + Lt: 'lt', + Lv: 'lv', + Mk: 'mk', + Ml: 'ml', + Mn: 'mn', + Mr: 'mr', + Ms: 'ms', + My: 'my', + Nb: 'nb', + Ne: 'ne', + Nl: 'nl', + Nn: 'nn', + Os: 'os', + Pa: 'pa', + Pl: 'pl', + Pt: 'pt', + PtBr: 'pt-br', + Ro: 'ro', + Ru: 'ru', + Sk: 'sk', + Sl: 'sl', + Sq: 'sq', + Sr: 'sr', + SrLatn: 'sr-latn', + Sv: 'sv', + Sw: 'sw', + Ta: 'ta', + Te: 'te', + Tg: 'tg', + Th: 'th', + Tk: 'tk', + Tr: 'tr', + Tt: 'tt', + Udm: 'udm', + Uk: 'uk', + Ur: 'ur', + Uz: 'uz', + Vi: 'vi', + ZhHans: 'zh-hans', + ZhHant: 'zh-hant' +} as const; +export type SchemaRetrieveLangEnum = typeof SchemaRetrieveLangEnum[keyof typeof SchemaRetrieveLangEnum]; diff --git a/democrasite-frontend/lib/auto/apis/UsersApi.ts b/democrasite-frontend/lib/auto/apis/UsersApi.ts new file mode 100644 index 0000000..4cb1500 --- /dev/null +++ b/democrasite-frontend/lib/auto/apis/UsersApi.ts @@ -0,0 +1,209 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Democrasite API + * Documentation of API endpoints of Democrasite + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import * as runtime from '../runtime'; +import type { + PatchedUser, + User, +} from '../models/index'; +import { + PatchedUserFromJSON, + PatchedUserToJSON, + UserFromJSON, + UserToJSON, +} from '../models/index'; + +export interface UsersPartialUpdateRequest { + username: string; + patchedUser?: PatchedUser; +} + +export interface UsersRetrieveRequest { + username: string; +} + +export interface UsersUpdateRequest { + username: string; + user: User; +} + +/** + * + */ +export class UsersApi extends runtime.BaseAPI { + + /** + */ + async usersListRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/users/`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(UserFromJSON)); + } + + /** + */ + async usersList(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const response = await this.usersListRaw(initOverrides); + return await response.value(); + } + + /** + */ + async usersMeRetrieveRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/users/me/`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => UserFromJSON(jsonValue)); + } + + /** + */ + async usersMeRetrieve(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.usersMeRetrieveRaw(initOverrides); + return await response.value(); + } + + /** + */ + async usersPartialUpdateRaw(requestParameters: UsersPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.username === null || requestParameters.username === undefined) { + throw new runtime.RequiredError('username','Required parameter requestParameters.username was null or undefined when calling usersPartialUpdate.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/users/{username}/`.replace(`{${"username"}}`, encodeURIComponent(String(requestParameters.username))), + method: 'PATCH', + headers: headerParameters, + query: queryParameters, + body: PatchedUserToJSON(requestParameters.patchedUser), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => UserFromJSON(jsonValue)); + } + + /** + */ + async usersPartialUpdate(requestParameters: UsersPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.usersPartialUpdateRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + */ + async usersRetrieveRaw(requestParameters: UsersRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.username === null || requestParameters.username === undefined) { + throw new runtime.RequiredError('username','Required parameter requestParameters.username was null or undefined when calling usersRetrieve.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/users/{username}/`.replace(`{${"username"}}`, encodeURIComponent(String(requestParameters.username))), + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => UserFromJSON(jsonValue)); + } + + /** + */ + async usersRetrieve(requestParameters: UsersRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.usersRetrieveRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + */ + async usersUpdateRaw(requestParameters: UsersUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.username === null || requestParameters.username === undefined) { + throw new runtime.RequiredError('username','Required parameter requestParameters.username was null or undefined when calling usersUpdate.'); + } + + if (requestParameters.user === null || requestParameters.user === undefined) { + throw new runtime.RequiredError('user','Required parameter requestParameters.user was null or undefined when calling usersUpdate.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // tokenAuth authentication + } + + const response = await this.request({ + path: `/api/users/{username}/`.replace(`{${"username"}}`, encodeURIComponent(String(requestParameters.username))), + method: 'PUT', + headers: headerParameters, + query: queryParameters, + body: UserToJSON(requestParameters.user), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => UserFromJSON(jsonValue)); + } + + /** + */ + async usersUpdate(requestParameters: UsersUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.usersUpdateRaw(requestParameters, initOverrides); + return await response.value(); + } + +} diff --git a/democrasite-frontend/lib/auto/apis/index.ts b/democrasite-frontend/lib/auto/apis/index.ts new file mode 100644 index 0000000..86a7402 --- /dev/null +++ b/democrasite-frontend/lib/auto/apis/index.ts @@ -0,0 +1,6 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './AuthApi'; +export * from './BillsApi'; +export * from './SchemaApi'; +export * from './UsersApi'; diff --git a/democrasite-frontend/api/auto/index.ts b/democrasite-frontend/lib/auto/index.ts similarity index 100% rename from democrasite-frontend/api/auto/index.ts rename to democrasite-frontend/lib/auto/index.ts diff --git a/democrasite-frontend/api/auto/models/Bill.ts b/democrasite-frontend/lib/auto/models/Bill.ts similarity index 88% rename from democrasite-frontend/api/auto/models/Bill.ts rename to democrasite-frontend/lib/auto/models/Bill.ts index da11661..bb25fa2 100644 --- a/democrasite-frontend/api/auto/models/Bill.ts +++ b/democrasite-frontend/lib/auto/models/Bill.ts @@ -68,6 +68,19 @@ export interface Bill { * @memberof Bill */ readonly noVotes: Array; + /** + * Return whether the user supports the bill. If the user is not authenticated + * or has not voted on this bill, return None. + * + * Args: + * bill: The bill to check + * + * Returns: + * Whether the user supports the bill, or None if not applicable + * @type {boolean} + * @memberof Bill + */ + readonly userSupports: boolean | null; /** * * @type {Date} @@ -111,6 +124,7 @@ export function instanceOfBill(value: object): boolean { isInstance = isInstance && "status" in value; isInstance = isInstance && "yesVotes" in value; isInstance = isInstance && "noVotes" in value; + isInstance = isInstance && "userSupports" in value; isInstance = isInstance && "created" in value; isInstance = isInstance && "name" in value; isInstance = isInstance && "constitutional" in value; @@ -134,6 +148,7 @@ export function BillFromJSONTyped(json: any, ignoreDiscriminator: boolean): Bill 'status': json['status'], 'yesVotes': json['yes_votes'], 'noVotes': json['no_votes'], + 'userSupports': json['user_supports'], 'created': (new Date(json['created'])), 'statusChanged': !exists(json, 'status_changed') ? undefined : (new Date(json['status_changed'])), 'name': json['name'], diff --git a/democrasite-frontend/api/auto/models/AuthToken.ts b/democrasite-frontend/lib/auto/models/Login.ts similarity index 58% rename from democrasite-frontend/api/auto/models/AuthToken.ts rename to democrasite-frontend/lib/auto/models/Login.ts index b4db4be..29c462a 100644 --- a/democrasite-frontend/api/auto/models/AuthToken.ts +++ b/democrasite-frontend/lib/auto/models/Login.ts @@ -16,58 +16,56 @@ import { exists, mapValues } from '../runtime'; /** * * @export - * @interface AuthToken + * @interface Login */ -export interface AuthToken { +export interface Login { /** * * @type {string} - * @memberof AuthToken + * @memberof Login */ - username: string; + username?: string; /** * * @type {string} - * @memberof AuthToken + * @memberof Login */ - password: string; + email?: string; /** * * @type {string} - * @memberof AuthToken + * @memberof Login */ - readonly token: string; + password: string; } /** - * Check if a given object implements the AuthToken interface. + * Check if a given object implements the Login interface. */ -export function instanceOfAuthToken(value: object): boolean { +export function instanceOfLogin(value: object): boolean { let isInstance = true; - isInstance = isInstance && "username" in value; isInstance = isInstance && "password" in value; - isInstance = isInstance && "token" in value; return isInstance; } -export function AuthTokenFromJSON(json: any): AuthToken { - return AuthTokenFromJSONTyped(json, false); +export function LoginFromJSON(json: any): Login { + return LoginFromJSONTyped(json, false); } -export function AuthTokenFromJSONTyped(json: any, ignoreDiscriminator: boolean): AuthToken { +export function LoginFromJSONTyped(json: any, ignoreDiscriminator: boolean): Login { if ((json === undefined) || (json === null)) { return json; } return { - 'username': json['username'], + 'username': !exists(json, 'username') ? undefined : json['username'], + 'email': !exists(json, 'email') ? undefined : json['email'], 'password': json['password'], - 'token': json['token'], }; } -export function AuthTokenToJSON(value?: AuthToken | null): any { +export function LoginToJSON(value?: Login | null): any { if (value === undefined) { return undefined; } @@ -77,6 +75,7 @@ export function AuthTokenToJSON(value?: AuthToken | null): any { return { 'username': value.username, + 'email': value.email, 'password': value.password, }; } diff --git a/democrasite-frontend/lib/auto/models/PasswordChange.ts b/democrasite-frontend/lib/auto/models/PasswordChange.ts new file mode 100644 index 0000000..fc8749f --- /dev/null +++ b/democrasite-frontend/lib/auto/models/PasswordChange.ts @@ -0,0 +1,74 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Democrasite API + * Documentation of API endpoints of Democrasite + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface PasswordChange + */ +export interface PasswordChange { + /** + * + * @type {string} + * @memberof PasswordChange + */ + newPassword1: string; + /** + * + * @type {string} + * @memberof PasswordChange + */ + newPassword2: string; +} + +/** + * Check if a given object implements the PasswordChange interface. + */ +export function instanceOfPasswordChange(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "newPassword1" in value; + isInstance = isInstance && "newPassword2" in value; + + return isInstance; +} + +export function PasswordChangeFromJSON(json: any): PasswordChange { + return PasswordChangeFromJSONTyped(json, false); +} + +export function PasswordChangeFromJSONTyped(json: any, ignoreDiscriminator: boolean): PasswordChange { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'newPassword1': json['new_password1'], + 'newPassword2': json['new_password2'], + }; +} + +export function PasswordChangeToJSON(value?: PasswordChange | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'new_password1': value.newPassword1, + 'new_password2': value.newPassword2, + }; +} diff --git a/democrasite-frontend/lib/auto/models/PasswordReset.ts b/democrasite-frontend/lib/auto/models/PasswordReset.ts new file mode 100644 index 0000000..5b2aec5 --- /dev/null +++ b/democrasite-frontend/lib/auto/models/PasswordReset.ts @@ -0,0 +1,65 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Democrasite API + * Documentation of API endpoints of Democrasite + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * Serializer for requesting a password reset e-mail. + * @export + * @interface PasswordReset + */ +export interface PasswordReset { + /** + * + * @type {string} + * @memberof PasswordReset + */ + email: string; +} + +/** + * Check if a given object implements the PasswordReset interface. + */ +export function instanceOfPasswordReset(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "email" in value; + + return isInstance; +} + +export function PasswordResetFromJSON(json: any): PasswordReset { + return PasswordResetFromJSONTyped(json, false); +} + +export function PasswordResetFromJSONTyped(json: any, ignoreDiscriminator: boolean): PasswordReset { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'email': json['email'], + }; +} + +export function PasswordResetToJSON(value?: PasswordReset | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'email': value.email, + }; +} diff --git a/democrasite-frontend/lib/auto/models/PasswordResetConfirm.ts b/democrasite-frontend/lib/auto/models/PasswordResetConfirm.ts new file mode 100644 index 0000000..c53b68c --- /dev/null +++ b/democrasite-frontend/lib/auto/models/PasswordResetConfirm.ts @@ -0,0 +1,92 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Democrasite API + * Documentation of API endpoints of Democrasite + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * Serializer for confirming a password reset attempt. + * @export + * @interface PasswordResetConfirm + */ +export interface PasswordResetConfirm { + /** + * + * @type {string} + * @memberof PasswordResetConfirm + */ + newPassword1: string; + /** + * + * @type {string} + * @memberof PasswordResetConfirm + */ + newPassword2: string; + /** + * + * @type {string} + * @memberof PasswordResetConfirm + */ + uid: string; + /** + * + * @type {string} + * @memberof PasswordResetConfirm + */ + token: string; +} + +/** + * Check if a given object implements the PasswordResetConfirm interface. + */ +export function instanceOfPasswordResetConfirm(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "newPassword1" in value; + isInstance = isInstance && "newPassword2" in value; + isInstance = isInstance && "uid" in value; + isInstance = isInstance && "token" in value; + + return isInstance; +} + +export function PasswordResetConfirmFromJSON(json: any): PasswordResetConfirm { + return PasswordResetConfirmFromJSONTyped(json, false); +} + +export function PasswordResetConfirmFromJSONTyped(json: any, ignoreDiscriminator: boolean): PasswordResetConfirm { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'newPassword1': json['new_password1'], + 'newPassword2': json['new_password2'], + 'uid': json['uid'], + 'token': json['token'], + }; +} + +export function PasswordResetConfirmToJSON(value?: PasswordResetConfirm | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'new_password1': value.newPassword1, + 'new_password2': value.newPassword2, + 'uid': value.uid, + 'token': value.token, + }; +} diff --git a/democrasite-frontend/api/auto/models/PatchedBill.ts b/democrasite-frontend/lib/auto/models/PatchedBill.ts similarity index 88% rename from democrasite-frontend/api/auto/models/PatchedBill.ts rename to democrasite-frontend/lib/auto/models/PatchedBill.ts index 07daa2c..6e5338c 100644 --- a/democrasite-frontend/api/auto/models/PatchedBill.ts +++ b/democrasite-frontend/lib/auto/models/PatchedBill.ts @@ -68,6 +68,19 @@ export interface PatchedBill { * @memberof PatchedBill */ readonly noVotes?: Array; + /** + * Return whether the user supports the bill. If the user is not authenticated + * or has not voted on this bill, return None. + * + * Args: + * bill: The bill to check + * + * Returns: + * Whether the user supports the bill, or None if not applicable + * @type {boolean} + * @memberof PatchedBill + */ + readonly userSupports?: boolean | null; /** * * @type {Date} @@ -125,6 +138,7 @@ export function PatchedBillFromJSONTyped(json: any, ignoreDiscriminator: boolean 'status': !exists(json, 'status') ? undefined : json['status'], 'yesVotes': !exists(json, 'yes_votes') ? undefined : json['yes_votes'], 'noVotes': !exists(json, 'no_votes') ? undefined : json['no_votes'], + 'userSupports': !exists(json, 'user_supports') ? undefined : json['user_supports'], 'created': !exists(json, 'created') ? undefined : (new Date(json['created'])), 'statusChanged': !exists(json, 'status_changed') ? undefined : (new Date(json['status_changed'])), 'name': !exists(json, 'name') ? undefined : json['name'], diff --git a/democrasite-frontend/api/auto/models/PatchedUser.ts b/democrasite-frontend/lib/auto/models/PatchedUser.ts similarity index 100% rename from democrasite-frontend/api/auto/models/PatchedUser.ts rename to democrasite-frontend/lib/auto/models/PatchedUser.ts diff --git a/democrasite-frontend/lib/auto/models/PatchedUserDetails.ts b/democrasite-frontend/lib/auto/models/PatchedUserDetails.ts new file mode 100644 index 0000000..107041e --- /dev/null +++ b/democrasite-frontend/lib/auto/models/PatchedUserDetails.ts @@ -0,0 +1,92 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Democrasite API + * Documentation of API endpoints of Democrasite + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * User model w/o password + * @export + * @interface PatchedUserDetails + */ +export interface PatchedUserDetails { + /** + * + * @type {number} + * @memberof PatchedUserDetails + */ + readonly pk?: number; + /** + * Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only. + * @type {string} + * @memberof PatchedUserDetails + */ + username?: string; + /** + * + * @type {string} + * @memberof PatchedUserDetails + */ + readonly email?: string; + /** + * + * @type {string} + * @memberof PatchedUserDetails + */ + readonly firstName?: string; + /** + * + * @type {string} + * @memberof PatchedUserDetails + */ + readonly lastName?: string; +} + +/** + * Check if a given object implements the PatchedUserDetails interface. + */ +export function instanceOfPatchedUserDetails(value: object): boolean { + let isInstance = true; + + return isInstance; +} + +export function PatchedUserDetailsFromJSON(json: any): PatchedUserDetails { + return PatchedUserDetailsFromJSONTyped(json, false); +} + +export function PatchedUserDetailsFromJSONTyped(json: any, ignoreDiscriminator: boolean): PatchedUserDetails { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'pk': !exists(json, 'pk') ? undefined : json['pk'], + 'username': !exists(json, 'username') ? undefined : json['username'], + 'email': !exists(json, 'email') ? undefined : json['email'], + 'firstName': !exists(json, 'first_name') ? undefined : json['first_name'], + 'lastName': !exists(json, 'last_name') ? undefined : json['last_name'], + }; +} + +export function PatchedUserDetailsToJSON(value?: PatchedUserDetails | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'username': value.username, + }; +} diff --git a/democrasite-frontend/api/auto/models/PullRequest.ts b/democrasite-frontend/lib/auto/models/PullRequest.ts similarity index 100% rename from democrasite-frontend/api/auto/models/PullRequest.ts rename to democrasite-frontend/lib/auto/models/PullRequest.ts diff --git a/democrasite-frontend/lib/auto/models/RestAuthDetail.ts b/democrasite-frontend/lib/auto/models/RestAuthDetail.ts new file mode 100644 index 0000000..230972b --- /dev/null +++ b/democrasite-frontend/lib/auto/models/RestAuthDetail.ts @@ -0,0 +1,64 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Democrasite API + * Documentation of API endpoints of Democrasite + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface RestAuthDetail + */ +export interface RestAuthDetail { + /** + * + * @type {string} + * @memberof RestAuthDetail + */ + readonly detail: string; +} + +/** + * Check if a given object implements the RestAuthDetail interface. + */ +export function instanceOfRestAuthDetail(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "detail" in value; + + return isInstance; +} + +export function RestAuthDetailFromJSON(json: any): RestAuthDetail { + return RestAuthDetailFromJSONTyped(json, false); +} + +export function RestAuthDetailFromJSONTyped(json: any, ignoreDiscriminator: boolean): RestAuthDetail { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'detail': json['detail'], + }; +} + +export function RestAuthDetailToJSON(value?: RestAuthDetail | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + }; +} diff --git a/democrasite-frontend/lib/auto/models/SocialLogin.ts b/democrasite-frontend/lib/auto/models/SocialLogin.ts new file mode 100644 index 0000000..bf2a093 --- /dev/null +++ b/democrasite-frontend/lib/auto/models/SocialLogin.ts @@ -0,0 +1,80 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Democrasite API + * Documentation of API endpoints of Democrasite + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface SocialLogin + */ +export interface SocialLogin { + /** + * + * @type {string} + * @memberof SocialLogin + */ + accessToken?: string; + /** + * + * @type {string} + * @memberof SocialLogin + */ + code?: string; + /** + * + * @type {string} + * @memberof SocialLogin + */ + idToken?: string; +} + +/** + * Check if a given object implements the SocialLogin interface. + */ +export function instanceOfSocialLogin(value: object): boolean { + let isInstance = true; + + return isInstance; +} + +export function SocialLoginFromJSON(json: any): SocialLogin { + return SocialLoginFromJSONTyped(json, false); +} + +export function SocialLoginFromJSONTyped(json: any, ignoreDiscriminator: boolean): SocialLogin { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'accessToken': !exists(json, 'access_token') ? undefined : json['access_token'], + 'code': !exists(json, 'code') ? undefined : json['code'], + 'idToken': !exists(json, 'id_token') ? undefined : json['id_token'], + }; +} + +export function SocialLoginToJSON(value?: SocialLogin | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'access_token': value.accessToken, + 'code': value.code, + 'id_token': value.idToken, + }; +} diff --git a/democrasite-frontend/lib/auto/models/Token.ts b/democrasite-frontend/lib/auto/models/Token.ts new file mode 100644 index 0000000..b880ba1 --- /dev/null +++ b/democrasite-frontend/lib/auto/models/Token.ts @@ -0,0 +1,65 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Democrasite API + * Documentation of API endpoints of Democrasite + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * Serializer for Token model. + * @export + * @interface Token + */ +export interface Token { + /** + * + * @type {string} + * @memberof Token + */ + key: string; +} + +/** + * Check if a given object implements the Token interface. + */ +export function instanceOfToken(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "key" in value; + + return isInstance; +} + +export function TokenFromJSON(json: any): Token { + return TokenFromJSONTyped(json, false); +} + +export function TokenFromJSONTyped(json: any, ignoreDiscriminator: boolean): Token { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'key': json['key'], + }; +} + +export function TokenToJSON(value?: Token | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'key': value.key, + }; +} diff --git a/democrasite-frontend/api/auto/models/User.ts b/democrasite-frontend/lib/auto/models/User.ts similarity index 100% rename from democrasite-frontend/api/auto/models/User.ts rename to democrasite-frontend/lib/auto/models/User.ts diff --git a/democrasite-frontend/lib/auto/models/UserDetails.ts b/democrasite-frontend/lib/auto/models/UserDetails.ts new file mode 100644 index 0000000..a98d4bc --- /dev/null +++ b/democrasite-frontend/lib/auto/models/UserDetails.ts @@ -0,0 +1,97 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Democrasite API + * Documentation of API endpoints of Democrasite + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * User model w/o password + * @export + * @interface UserDetails + */ +export interface UserDetails { + /** + * + * @type {number} + * @memberof UserDetails + */ + readonly pk: number; + /** + * Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only. + * @type {string} + * @memberof UserDetails + */ + username: string; + /** + * + * @type {string} + * @memberof UserDetails + */ + readonly email: string; + /** + * + * @type {string} + * @memberof UserDetails + */ + readonly firstName: string; + /** + * + * @type {string} + * @memberof UserDetails + */ + readonly lastName: string; +} + +/** + * Check if a given object implements the UserDetails interface. + */ +export function instanceOfUserDetails(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "pk" in value; + isInstance = isInstance && "username" in value; + isInstance = isInstance && "email" in value; + isInstance = isInstance && "firstName" in value; + isInstance = isInstance && "lastName" in value; + + return isInstance; +} + +export function UserDetailsFromJSON(json: any): UserDetails { + return UserDetailsFromJSONTyped(json, false); +} + +export function UserDetailsFromJSONTyped(json: any, ignoreDiscriminator: boolean): UserDetails { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'pk': json['pk'], + 'username': json['username'], + 'email': json['email'], + 'firstName': json['first_name'], + 'lastName': json['last_name'], + }; +} + +export function UserDetailsToJSON(value?: UserDetails | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'username': value.username, + }; +} diff --git a/democrasite-frontend/lib/auto/models/index.ts b/democrasite-frontend/lib/auto/models/index.ts new file mode 100644 index 0000000..83ec10f --- /dev/null +++ b/democrasite-frontend/lib/auto/models/index.ts @@ -0,0 +1,16 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './Bill'; +export * from './Login'; +export * from './PasswordChange'; +export * from './PasswordReset'; +export * from './PasswordResetConfirm'; +export * from './PatchedBill'; +export * from './PatchedUser'; +export * from './PatchedUserDetails'; +export * from './PullRequest'; +export * from './RestAuthDetail'; +export * from './SocialLogin'; +export * from './Token'; +export * from './User'; +export * from './UserDetails'; diff --git a/democrasite-frontend/api/auto/runtime.ts b/democrasite-frontend/lib/auto/runtime.ts similarity index 100% rename from democrasite-frontend/api/auto/runtime.ts rename to democrasite-frontend/lib/auto/runtime.ts diff --git a/democrasite-frontend/lib/fetch_bills.tsx b/democrasite-frontend/lib/fetch_bills.tsx deleted file mode 100644 index 0c11f1b..0000000 --- a/democrasite-frontend/lib/fetch_bills.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export default function fetchBills() { - return fetch(`${process.env.BASE_API_URL}/bills/`, { - next: { revalidate: 60 }, // revalidate every minute for testing - }).then((response) => response.json()); -} - -export function fetchBill(id: number) { - return fetch(`${process.env.BASE_API_URL}/bills/${id}`, { - cache: "no-store", // no caching for testing (probably want to cache info but not vote data) - }).then((response) => response.json()); -} diff --git a/democrasite/users/api/views.py b/democrasite/users/api/views.py index d28633c..551123c 100644 --- a/democrasite/users/api/views.py +++ b/democrasite/users/api/views.py @@ -1,3 +1,9 @@ +from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter +from allauth.socialaccount.providers.oauth2.client import OAuth2Client +from dj_rest_auth.registration.views import SocialLoginView +from dj_rest_auth.serializers import TokenSerializer +from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import extend_schema_view from rest_framework import status from rest_framework.decorators import action from rest_framework.mixins import ListModelMixin @@ -24,3 +30,19 @@ def get_queryset(self, *args, **kwargs): def me(self, request): serializer = UserSerializer(request.user, context={"request": request}) return Response(status=status.HTTP_200_OK, data=serializer.data) + + +# dj-rest-auth views + + +@extend_schema_view( + post=extend_schema( + summary="Login with GitHub", + description="Login with GitHub using OAuth2", + responses=TokenSerializer, + ) +) +class GitHubLogin(SocialLoginView): + adapter_class = GitHubOAuth2Adapter + callback_url = "http://localhost:3000/api/auth/github" + client_class = OAuth2Client diff --git a/democrasite/webiscite/api/serializers.py b/democrasite/webiscite/api/serializers.py index e2f3528..9e16d67 100644 --- a/democrasite/webiscite/api/serializers.py +++ b/democrasite/webiscite/api/serializers.py @@ -2,6 +2,7 @@ from rest_framework.serializers import CharField from rest_framework.serializers import ModelSerializer +from rest_framework.serializers import SerializerMethodField from rest_framework.serializers import SlugRelatedField from democrasite.users.api.serializers import UserSerializer @@ -37,6 +38,7 @@ class BillSerializer(ModelSerializer): no_votes: "SlugRelatedField[User]" = SlugRelatedField( many=True, read_only=True, slug_field="username" ) + user_supports = SerializerMethodField(required=False) class Meta: model = Bill @@ -49,4 +51,17 @@ class Meta: "created", "yes_votes", "no_votes", + "user_supports", ] + + def get_user_supports(self, bill: Bill) -> bool | None: + """Return whether the user supports the bill. If the user is not authenticated + or has not voted on this bill, return None. + + Args: + bill: The bill to check + + Returns: + Whether the user supports the bill, or None if not applicable""" + user: User = self.context["user"] + return bill.user_supports(user) if user.is_authenticated else None diff --git a/democrasite/webiscite/api/views.py b/democrasite/webiscite/api/views.py index cb6d139..f679646 100644 --- a/democrasite/webiscite/api/views.py +++ b/democrasite/webiscite/api/views.py @@ -1,12 +1,8 @@ -import contextlib from typing import Any -from django.db.models import Prefetch from rest_framework.mixins import ListModelMixin from rest_framework.mixins import RetrieveModelMixin from rest_framework.mixins import UpdateModelMixin -from rest_framework.request import Request -from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet from democrasite.webiscite.models import Bill @@ -18,36 +14,9 @@ class BillViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, GenericV serializer_class = BillSerializer queryset = Bill.objects.select_related("author", "pull_request") - def retrieve(self, request: Request, *args: Any, **kwargs: Any) -> Response: - response = super().retrieve(request, *args, **kwargs) + def get_serializer_context(self) -> dict[str, Any]: + context = super().get_serializer_context() + context["user"] = self.request.user + return context - if request.user.is_authenticated: - bill: Bill = self.get_object() - with contextlib.suppress(bill.vote_set.model.DoesNotExist): - response.data["user_supports"] = bill.vote_set.get( - user=request.user - ).support - - return response - - def list(self, request: Request, *args: Any, **kwargs: Any) -> Response: - response = super().list(request, *args, **kwargs) - - if request.user.is_authenticated: - user_votes_prefetch = Prefetch( - "vote_set", queryset=request.user.vote_set.all(), to_attr="user_votes" - ) - - bill_ids = [bill["id"] for bill in response.data] - # Need a new database call in order to prefetch the user's votes - bills = Bill.objects.filter(id__in=bill_ids).prefetch_related( - user_votes_prefetch - ) - for bill, bill_dict in zip(bills, response.data, strict=True): - vote_query = bill.user_votes # type: ignore [attr-defined] - - # Guaranteed to be at most one element, avoids calling database again - for vote in vote_query: - bill_dict["user_supports"] = vote.support - - return response + # TODO: Prefetch bill list votes diff --git a/democrasite/webiscite/models.py b/democrasite/webiscite/models.py index 4b07fc6..d9abc81 100644 --- a/democrasite/webiscite/models.py +++ b/democrasite/webiscite/models.py @@ -3,6 +3,8 @@ import json from logging import getLogger from typing import Any +from typing import Self +from typing import cast import requests from django.conf import settings @@ -57,7 +59,7 @@ def __str__(self) -> str: return f"PR #{self.number}" @classmethod - def create_from_pr(cls, pr: dict[str, Any]) -> "PullRequest": + def create_from_pr(cls, pr: dict[str, Any]) -> Self: """Update or create a pull request from a parsed JSON object representing the it If the given pull request exists, update it based on the request, otherwise @@ -84,7 +86,8 @@ def create_from_pr(cls, pr: dict[str, Any]) -> "PullRequest": act = "created" if created else "updated" logger.info("PR %s: Pull request %s", pr["number"], act) - return pull_request + + return cast(Self, pull_request) # mypy infers wrong type (Pylance doesn't 👀) def close(self) -> "Bill | None": """Close a pull request and update the local representation @@ -148,9 +151,6 @@ class Status(models.TextChoices): PeriodicTask, on_delete=models.PROTECT, null=True, blank=True ) - # TODO: Add a custom manager with a queryset method to get open bills - # (get manager from queryset) - class Meta: constraints = [ models.UniqueConstraint( @@ -179,7 +179,7 @@ def yes_votes(self) -> models.QuerySet[User]: def no_votes(self) -> models.QuerySet[User]: return self.votes.filter(vote__support=False) - def vote(self, user: User, *, support: bool): + def vote(self, user: User, *, support: bool) -> None: """Sets the given user's vote based on the support parameter If the user already voted the way the method would set, their vote is @@ -199,10 +199,28 @@ def vote(self, user: User, *, support: bool): except Vote.DoesNotExist: # Stubs issue fixed (by me!) in https://github.com/typeddjango/django-stubs/pull/1943 # Just waiting for new version to be released - self.votes.add(user, through_defaults={"support": support}) # type: ignore[arg-type,call-arg] + self.votes.add(user, through_defaults={"support": support}) # type: ignore[call-arg] + + def user_supports(self, user: User) -> bool | None: + """ + Returns whether the given user supports, opposes, or has not voted on this bill + + Args: + user: The user to check + + Returns: + True if the user supports the bill, False if they oppose it, and None if + they have not voted + """ + try: + vote: Vote = self.vote_set.get(user=user) + except Vote.DoesNotExist: + return None + else: + return vote.support @classmethod - def create_from_pr(cls, pr: dict[str, Any]) -> "tuple[PullRequest, Bill | None]": + def create_from_pr(cls, pr: dict[str, Any]) -> tuple[PullRequest, Self | None]: """Create a :class:`~democrasite.webiscite.models.PullRequest` and, if the creator has an account, :class:`~democrasite.webiscite.models.Bill` instance from a pull request @@ -227,7 +245,7 @@ def create_from_pr(cls, pr: dict[str, Any]) -> "tuple[PullRequest, Bill | None]" return pull_request, None # TODO: everything below here could be in BillManager - bill = Bill(**bill_kwargs) + bill = cls(**bill_kwargs) bill.full_clean() bill.save() logger.info("PR %s: Bill %s created", pr["number"], bill.id) diff --git a/democrasite/webiscite/views.py b/democrasite/webiscite/views.py index b9ec63b..63c389d 100644 --- a/democrasite/webiscite/views.py +++ b/democrasite/webiscite/views.py @@ -20,7 +20,7 @@ class BillListView(ListView): """View listing all open bills. Used for webiscite:index.""" model = Bill - queryset = Bill.open.all() # type: ignore [attr-defined] # comes from django_model_utils + queryset = Bill.open.all() # comes from django_model_utils def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) diff --git a/requirements/base.txt b/requirements/base.txt index 971d0e2..7c88ec4 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -21,6 +21,7 @@ django-redis==5.4.0 # https://github.com/jazzband/django-redis # Django REST Framework djangorestframework==3.14.0 # https://github.com/encode/django-rest-framework django-cors-headers==4.3.1 # https://github.com/adamchainz/django-cors-headers +dj-rest-auth==5.0.2 # https://github.com/iMerica/dj-rest-auth # DRF-spectacular for api documentation drf-spectacular==0.27.1 # https://github.com/tfranzel/drf-spectacular diff --git a/schema.json b/schema.json index 35cac42..1fb0163 100644 --- a/schema.json +++ b/schema.json @@ -6,11 +6,412 @@ "description": "Documentation of API endpoints of Democrasite" }, "paths": { + "/api/auth/github/": { + "post": { + "operationId": "auth_github_create", + "description": "Login with GitHub using OAuth2", + "summary": "Login with GitHub", + "tags": [ + "auth" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SocialLogin" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/SocialLogin" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/SocialLogin" + } + } + } + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + }, + {} + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Token" + } + } + }, + "description": "" + } + } + } + }, + "/api/auth/login/": { + "post": { + "operationId": "auth_login_create", + "description": "Check the credentials and return the REST Token\nif the credentials are valid and authenticated.\nCalls Django Auth login method to register User ID\nin Django session framework\n\nAccept the following POST parameters: username, password\nReturn the REST Framework Token Object's key.", + "tags": [ + "auth" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Login" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Login" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Login" + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + }, + {} + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Token" + } + } + }, + "description": "" + } + } + } + }, + "/api/auth/logout/": { + "post": { + "operationId": "auth_logout_create", + "description": "Calls Django logout method and delete the Token object\nassigned to the current User object.\n\nAccepts/Returns nothing.", + "tags": [ + "auth" + ], + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + }, + {} + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestAuthDetail" + } + } + }, + "description": "" + } + } + } + }, + "/api/auth/password/change/": { + "post": { + "operationId": "auth_password_change_create", + "description": "Calls Django Auth SetPasswordForm save method.\n\nAccepts the following POST parameters: new_password1, new_password2\nReturns the success/fail message.", + "tags": [ + "auth" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PasswordChange" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PasswordChange" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/PasswordChange" + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestAuthDetail" + } + } + }, + "description": "" + } + } + } + }, + "/api/auth/password/reset/": { + "post": { + "operationId": "auth_password_reset_create", + "description": "Calls Django Auth PasswordResetForm save method.\n\nAccepts the following POST parameters: email\nReturns the success/fail message.", + "tags": [ + "auth" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PasswordReset" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PasswordReset" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/PasswordReset" + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + }, + {} + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestAuthDetail" + } + } + }, + "description": "" + } + } + } + }, + "/api/auth/password/reset/confirm/": { + "post": { + "operationId": "auth_password_reset_confirm_create", + "description": "Password reset e-mail link is confirmed, therefore\nthis resets the user's password.\n\nAccepts the following POST parameters: token, uid,\n new_password1, new_password2\nReturns the success/fail message.", + "tags": [ + "auth" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PasswordResetConfirm" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PasswordResetConfirm" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/PasswordResetConfirm" + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + }, + {} + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestAuthDetail" + } + } + }, + "description": "" + } + } + } + }, + "/api/auth/user/": { + "get": { + "operationId": "auth_user_retrieve", + "description": "Reads and updates UserModel fields\nAccepts GET, PUT, PATCH methods.\n\nDefault accepted fields: username, first_name, last_name\nDefault display fields: pk, username, email, first_name, last_name\nRead-only fields: pk, email\n\nReturns UserModel fields.", + "tags": [ + "auth" + ], + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDetails" + } + } + }, + "description": "" + } + } + }, + "put": { + "operationId": "auth_user_update", + "description": "Reads and updates UserModel fields\nAccepts GET, PUT, PATCH methods.\n\nDefault accepted fields: username, first_name, last_name\nDefault display fields: pk, username, email, first_name, last_name\nRead-only fields: pk, email\n\nReturns UserModel fields.", + "tags": [ + "auth" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDetails" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/UserDetails" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/UserDetails" + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDetails" + } + } + }, + "description": "" + } + } + }, + "patch": { + "operationId": "auth_user_partial_update", + "description": "Reads and updates UserModel fields\nAccepts GET, PUT, PATCH methods.\n\nDefault accepted fields: username, first_name, last_name\nDefault display fields: pk, username, email, first_name, last_name\nRead-only fields: pk, email\n\nReturns UserModel fields.", + "tags": [ + "auth" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PatchedUserDetails" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PatchedUserDetails" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/PatchedUserDetails" + } + } + } + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDetails" + } + } + }, + "description": "" + } + } + } + }, "/api/bills/": { "get": { - "operationId": "api_bills_list", + "operationId": "bills_list", "tags": [ - "api" + "bills" ], "security": [ { @@ -40,7 +441,7 @@ }, "/api/bills/{id}/": { "get": { - "operationId": "api_bills_retrieve", + "operationId": "bills_retrieve", "parameters": [ { "in": "path", @@ -53,7 +454,7 @@ } ], "tags": [ - "api" + "bills" ], "security": [ { @@ -78,7 +479,7 @@ } }, "put": { - "operationId": "api_bills_update", + "operationId": "bills_update", "parameters": [ { "in": "path", @@ -91,7 +492,7 @@ } ], "tags": [ - "api" + "bills" ], "requestBody": { "content": { @@ -135,7 +536,7 @@ } }, "patch": { - "operationId": "api_bills_partial_update", + "operationId": "bills_partial_update", "parameters": [ { "in": "path", @@ -148,7 +549,7 @@ } ], "tags": [ - "api" + "bills" ], "requestBody": { "content": { @@ -193,7 +594,7 @@ }, "/api/schema/": { "get": { - "operationId": "api_schema_retrieve", + "operationId": "schema_retrieve", "description": "OpenApi3 schema for this API. Format can be selected via content negotiation.\n\n- YAML: application/vnd.oai.openapi\n- JSON: application/vnd.oai.openapi+json", "parameters": [ { @@ -316,7 +717,7 @@ } ], "tags": [ - "api" + "schema" ], "security": [ { @@ -361,9 +762,9 @@ }, "/api/users/": { "get": { - "operationId": "api_users_list", + "operationId": "users_list", "tags": [ - "api" + "users" ], "security": [ { @@ -393,7 +794,7 @@ }, "/api/users/{username}/": { "get": { - "operationId": "api_users_retrieve", + "operationId": "users_retrieve", "parameters": [ { "in": "path", @@ -406,7 +807,7 @@ } ], "tags": [ - "api" + "users" ], "security": [ { @@ -431,7 +832,7 @@ } }, "put": { - "operationId": "api_users_update", + "operationId": "users_update", "parameters": [ { "in": "path", @@ -444,7 +845,7 @@ } ], "tags": [ - "api" + "users" ], "requestBody": { "content": { @@ -488,7 +889,7 @@ } }, "patch": { - "operationId": "api_users_partial_update", + "operationId": "users_partial_update", "parameters": [ { "in": "path", @@ -501,7 +902,7 @@ } ], "tags": [ - "api" + "users" ], "requestBody": { "content": { @@ -546,9 +947,9 @@ }, "/api/users/me/": { "get": { - "operationId": "api_users_me_retrieve", + "operationId": "users_me_retrieve", "tags": [ - "api" + "users" ], "security": [ { @@ -572,80 +973,10 @@ } } } - }, - "/auth-token/": { - "post": { - "operationId": "auth_token_create", - "tags": [ - "auth-token" - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/AuthToken" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/AuthToken" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/AuthToken" - } - } - }, - "required": true - }, - "security": [ - { - "cookieAuth": [] - }, - { - "tokenAuth": [] - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AuthToken" - } - } - }, - "description": "" - } - } - } } }, "components": { "schemas": { - "AuthToken": { - "type": "object", - "properties": { - "username": { - "type": "string", - "writeOnly": true - }, - "password": { - "type": "string", - "writeOnly": true - }, - "token": { - "type": "string", - "readOnly": true - } - }, - "required": [ - "password", - "token", - "username" - ] - }, "Bill": { "type": "object", "properties": { @@ -681,6 +1012,12 @@ }, "readOnly": true }, + "user_supports": { + "type": "boolean", + "nullable": true, + "description": "Return whether the user supports the bill. If the user is not authenticated\nor has not voted on this bill, return None.\n\nArgs:\n bill: The bill to check\n\nReturns:\n Whether the user supports the bill, or None if not applicable", + "readOnly": true + }, "created": { "type": "string", "format": "date-time", @@ -712,9 +1049,84 @@ "no_votes", "pull_request", "status", + "user_supports", "yes_votes" ] }, + "Login": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "password": { + "type": "string" + } + }, + "required": [ + "password" + ] + }, + "PasswordChange": { + "type": "object", + "properties": { + "new_password1": { + "type": "string", + "maxLength": 128 + }, + "new_password2": { + "type": "string", + "maxLength": 128 + } + }, + "required": [ + "new_password1", + "new_password2" + ] + }, + "PasswordReset": { + "type": "object", + "description": "Serializer for requesting a password reset e-mail.", + "properties": { + "email": { + "type": "string", + "format": "email" + } + }, + "required": [ + "email" + ] + }, + "PasswordResetConfirm": { + "type": "object", + "description": "Serializer for confirming a password reset attempt.", + "properties": { + "new_password1": { + "type": "string", + "maxLength": 128 + }, + "new_password2": { + "type": "string", + "maxLength": 128 + }, + "uid": { + "type": "string" + }, + "token": { + "type": "string" + } + }, + "required": [ + "new_password1", + "new_password2", + "token", + "uid" + ] + }, "PatchedBill": { "type": "object", "properties": { @@ -750,6 +1162,12 @@ }, "readOnly": true }, + "user_supports": { + "type": "boolean", + "nullable": true, + "description": "Return whether the user supports the bill. If the user is not authenticated\nor has not voted on this bill, return None.\n\nArgs:\n bill: The bill to check\n\nReturns:\n Whether the user supports the bill, or None if not applicable", + "readOnly": true + }, "created": { "type": "string", "format": "date-time", @@ -794,6 +1212,37 @@ } } }, + "PatchedUserDetails": { + "type": "object", + "description": "User model w/o password", + "properties": { + "pk": { + "type": "integer", + "readOnly": true, + "title": "ID" + }, + "username": { + "type": "string", + "description": "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + "pattern": "^[\\w.@+-]+$", + "maxLength": 150 + }, + "email": { + "type": "string", + "format": "email", + "readOnly": true, + "title": "Email address" + }, + "first_name": { + "type": "string", + "readOnly": true + }, + "last_name": { + "type": "string", + "readOnly": true + } + } + }, "PullRequest": { "type": "object", "properties": { @@ -837,6 +1286,45 @@ "title" ] }, + "RestAuthDetail": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "readOnly": true + } + }, + "required": [ + "detail" + ] + }, + "SocialLogin": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "code": { + "type": "string" + }, + "id_token": { + "type": "string" + } + } + }, + "Token": { + "type": "object", + "description": "Serializer for Token model.", + "properties": { + "key": { + "type": "string", + "maxLength": 40 + } + }, + "required": [ + "key" + ] + }, "User": { "type": "object", "properties": { @@ -861,6 +1349,44 @@ "url", "username" ] + }, + "UserDetails": { + "type": "object", + "description": "User model w/o password", + "properties": { + "pk": { + "type": "integer", + "readOnly": true, + "title": "ID" + }, + "username": { + "type": "string", + "description": "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + "pattern": "^[\\w.@+-]+$", + "maxLength": 150 + }, + "email": { + "type": "string", + "format": "email", + "readOnly": true, + "title": "Email address" + }, + "first_name": { + "type": "string", + "readOnly": true + }, + "last_name": { + "type": "string", + "readOnly": true + } + }, + "required": [ + "email", + "first_name", + "last_name", + "pk", + "username" + ] } }, "securitySchemes": {