Skip to content

Commit 22a9070

Browse files
committed
Protect server with API Key
1 parent 6bf71a6 commit 22a9070

5 files changed

Lines changed: 62 additions & 12 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ services:
2525
container_name: opennumismatweb
2626
image: ghcr.io/opennumismat/opennumismatweb:latest
2727
restart: unless-stopped
28+
environment:
29+
API_KEY: <random_string> # optional
2830
ports:
2931
- 8000:8000
3032
volumes:
@@ -39,9 +41,13 @@ or docker run:
3941

4042
Look at Development section
4143

44+
### Set the necessary env vars
45+
46+
`API_KEY` - protects your server from unauthorized access. Generate a secure value using: `openssl rand -base64 32`
47+
4248
## Development
4349

44-
Run backend (optional):
50+
Setup env vars and run backend (optional):
4551

4652
```
4753
cd backend

backend/app.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,40 @@
11
import base64
2+
import os
23
import sqlite3
34
from io import BytesIO
4-
from fastapi import FastAPI
5+
from fastapi import FastAPI, HTTPException, Security, Depends
6+
from fastapi.security.api_key import APIKeyHeader
57
from fastapi.staticfiles import StaticFiles
68
from pathlib import Path
79
from PIL import Image
10+
from starlette import status
811
from _version import __version__
912

1013

1114
DATA_PATH = 'data'
1215
MAX_PREVIEW_IMAGE_HEIGHT = 54 * 4
1316

17+
API_KEY_NAME = "access_token"
18+
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
19+
API_KEY = os.getenv("API_KEY")
20+
1421
app = FastAPI(
1522
docs_url=None,
1623
redoc_url=None,
1724
openapi_url=None,
1825
)
1926

2027

28+
async def get_api_key(header_key: str = Security(api_key_header)):
29+
print(header_key)
30+
if header_key == API_KEY:
31+
return header_key
32+
raise HTTPException(
33+
status_code=status.HTTP_403_FORBIDDEN,
34+
detail="Could not validate API Key"
35+
)
36+
37+
2138
def sqlite_connect(file):
2239
file_uri = f"file:{DATA_PATH}/{file}?mode=ro"
2340
return sqlite3.connect(file_uri, uri=True)
@@ -28,7 +45,7 @@ def version():
2845
return __version__
2946

3047

31-
@app.get("/api/filelist")
48+
@app.get("/api/filelist", dependencies=[Depends(get_api_key)])
3249
def filelist():
3350
root = Path(DATA_PATH).resolve()
3451
db_files = []
@@ -42,7 +59,7 @@ def filelist():
4259
return db_files
4360

4461

45-
@app.get("/api/coins")
62+
@app.get("/api/coins", dependencies=[Depends(get_api_key)])
4663
def coins(f, search=None, sort=None, reverse: bool = False, status_filter=None, country_filter=None, series_filter=None, type_filter=None,
4764
period_filter=None, mint_filter=None):
4865
file = f
@@ -97,7 +114,7 @@ def coins(f, search=None, sort=None, reverse: bool = False, status_filter=None,
97114
return data
98115

99116

100-
@app.get("/api/images")
117+
@app.get("/api/images", dependencies=[Depends(get_api_key)])
101118
def coins(f):
102119
file = f
103120
con = sqlite_connect(file)
@@ -120,7 +137,7 @@ def coins(f):
120137
return data
121138

122139

123-
@app.get("/api/filters")
140+
@app.get("/api/filters", dependencies=[Depends(get_api_key)])
124141
def filters(f):
125142
file = f
126143
con = sqlite_connect(file)
@@ -141,7 +158,7 @@ def filters(f):
141158
return result
142159

143160

144-
@app.get("/api/coin_data")
161+
@app.get("/api/coin_data", dependencies=[Depends(get_api_key)])
145162
def coin_data(f, id):
146163
info_fields = ('coins.title', 'obverseimg.image', 'reverseimg.image',
147164
'status', 'region', 'country', 'period', 'ruler', 'value', 'unit', 'type',
@@ -170,7 +187,7 @@ def coin_data(f, id):
170187
return result
171188

172189

173-
@app.get("/api/photo")
190+
@app.get("/api/photo", dependencies=[Depends(get_api_key)])
174191
def photo(f, id, type):
175192
file = f
176193
coin_id = id
@@ -238,7 +255,7 @@ def photo(f, id, type):
238255
return result
239256

240257

241-
@app.get("/api/photos")
258+
@app.get("/api/photos", dependencies=[Depends(get_api_key)])
242259
def photos(f, id):
243260
file = f
244261
coin_id = id
@@ -266,7 +283,7 @@ def photos(f, id):
266283
return result
267284

268285

269-
@app.get("/api/settings")
286+
@app.get("/api/settings", dependencies=[Depends(get_api_key)])
270287
def settings(f):
271288
field_ids = {
272289
13: 'status',
@@ -332,7 +349,7 @@ def settings(f):
332349
return collection_settings
333350

334351

335-
@app.get("/api/summary")
352+
@app.get("/api/summary", dependencies=[Depends(get_api_key)])
336353
def summary(f):
337354
collection_summary = {}
338355

frontend/src/components/SettingsView.vue

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { useTheme } from 'vuetify'
44
import { languageList, setLocale } from '@/i18n'
55
import i18n from '../i18n'
66
import {appTitle} from "@/composables/appTitle.js"
7-
import { imagePresentation, statusPresentation, currentTheme } from "@/composables/useSettings";
7+
import { imagePresentation, statusPresentation, currentTheme, apiKey } from "@/composables/useSettings";
8+
9+
const isServerLess = import.meta.env.VITE_SERVERLESS;
810
911
const languageItems = Object.entries(languageList).map(([key, value]) => ({
1012
lang: key,
@@ -104,6 +106,20 @@ const handleThemeChange = (theme) => {
104106
</v-list-item>
105107
</v-list>
106108
</v-container>
109+
<v-container v-if="!isServerLess">
110+
<v-list>
111+
<v-list-item>
112+
<v-list-item-action start>
113+
<v-text-field
114+
label="API Key"
115+
v-model="apiKey"
116+
density="comfortable"
117+
hide-details
118+
/>
119+
</v-list-item-action>
120+
</v-list-item>
121+
</v-list>
122+
</v-container>
107123
</template>
108124

109125
<style scoped>

frontend/src/composables/useService.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ const api = axios.create({
1212
timeout: 20000,
1313
})
1414

15+
api.interceptors.request.use((config) => {
16+
const apiKey = localStorage.getItem('apiKey');
17+
18+
if (apiKey) {
19+
config.headers['access_token'] = apiKey;
20+
}
21+
22+
return config;
23+
});
24+
1525
let connection_type = null;
1626
let connected_file = null;
1727

frontend/src/composables/useSettings.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ import { useLocalStorage } from "@vueuse/core";
33
export const imagePresentation = useLocalStorage('image-presentation', 'image')
44
export const statusPresentation = useLocalStorage('status-presentation', 'text_icon')
55
export const currentTheme = useLocalStorage('theme', 'light')
6+
export const apiKey = useLocalStorage('apiKey', null )

0 commit comments

Comments
 (0)