Skip to content

Commit

Permalink
add encryption for connectors 🔐
Browse files Browse the repository at this point in the history
  • Loading branch information
KristopherKubicki committed May 15, 2023
1 parent 4ec2d4f commit 14ff44e
Show file tree
Hide file tree
Showing 14 changed files with 267 additions and 18 deletions.
1 change: 1 addition & 0 deletions .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install pylint
pip install -r requirements.txt
- name: Analysing the code with pylint
run: |
pylint $(git ls-files '*.py')
41 changes: 41 additions & 0 deletions app/api/api_v1/routers/vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from fastapi import APIRouter, HTTPException
from typing import List
from app import crud, models, schemas

router = APIRouter()

@router.post("/", response_model=schemas.Connector)
def create_connector(connector: schemas.ConnectorCreate):
# You'd use the cryptography library to encrypt the config details here before storing them in the database.
encrypted_config = encrypt_config(connector.config)
connector_in_db = crud.connector.create_with_encrypted_config(connector, encrypted_config)
return connector_in_db

@router.get("/{connector_id}", response_model=schemas.Connector)
def read_connector(connector_id: int):
connector_in_db = crud.connector.get(connector_id)
if connector_in_db is None:
raise HTTPException(status_code=404, detail="Connector not found")
# You'd use the cryptography library to decrypt the config details here before returning them.
decrypted_config = decrypt_config(connector_in_db.config)
connector_in_db.config = decrypted_config
return connector_in_db

@router.put("/{connector_id}", response_model=schemas.Connector)
def update_connector(connector_id: int, connector: schemas.ConnectorUpdate):
connector_in_db = crud.connector.get(connector_id)
if connector_in_db is None:
raise HTTPException(status_code=404, detail="Connector not found")
# You'd use the cryptography library to encrypt the config details here before storing them in the database.
encrypted_config = encrypt_config(connector.config)
connector_in_db = crud.connector.update_with_encrypted_config(connector_in_db, connector, encrypted_config)
return connector_in_db

@router.delete("/{connector_id}", response_model=schemas.Connector)
def delete_connector(connector_id: int):
connector_in_db = crud.connector.get(connector_id)
if connector_in_db is None:
raise HTTPException(status_code=404, detail="Connector not found")
crud.connector.remove(connector_id)
return connector_in_db

2 changes: 2 additions & 0 deletions app/app_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ async def get_messages_endpoint(bot_id: int, db: Session = Depends(get_async_db)
# TODO: support pagination
try:
messages = get_messages_by_bot_id(db=db, bot_id=bot_id)
for m in messages:
lt = Message.from_orm(m).dict()
return [Message.from_orm(message).dict() for message in messages]
except Exception as e:
print("Error:", e)
Expand Down
3 changes: 3 additions & 0 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Any, Dict, Optional
from pydantic import BaseSettings, validator

# should this move to schemas?
class Settings(BaseSettings):
secret_key: str
app_name: str
Expand Down Expand Up @@ -31,6 +32,8 @@ class Settings(BaseSettings):

access_token_expire_minutes: int
algorithm: str = "HS256"
encryption_key: str
encryption_salt: str

# Database
database_url: str
Expand Down
36 changes: 36 additions & 0 deletions app/core/encryption.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.backends import default_backend
from base64 import urlsafe_b64encode, urlsafe_b64decode
from app.core.config import get_settings

class EncryptionManager:
def __init__(self):
settings = get_settings()
key = settings.ENCRYPTION_KEY
salt = settings.ENCRYPTION_SALT
if key is None or salt is None:
raise ValueError('No encryption key or salt provided in config.')
self.key = key.encode()
self.salt = salt.encode()
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=self.salt,
iterations=100000,
backend=default_backend()
)
self.fernet_key = urlsafe_b64encode(kdf.derive(self.key))
self.fernet = Fernet(self.fernet_key)

def encrypt(self, data):
data = data.encode()
encrypted = self.fernet.encrypt(data)
return encrypted.decode()

def decrypt(self, encrypted_data):
encrypted_data = encrypted_data.encode()
decrypted = self.fernet.decrypt(encrypted_data)
return decrypted.decode()

18 changes: 18 additions & 0 deletions app/models/connectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from sqlalchemy import Column, Integer, String, DateTime, JSON
from sqlalchemy.sql import func
from app.database import Base


class Connector(Base):
__tablename__ = "connectors"

id = Column(Integer, primary_key=True, index=True)
description = Column(String)
connector_type = Column(String, index=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
last_message_sent = Column(DateTime(timezone=True))
last_message_received = Column(DateTime(timezone=True))
last_successful_message = Column(DateTime(timezone=True))
config = Column(JSON)

2 changes: 1 addition & 1 deletion app/models/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class Message(Base):
id = Column(Integer, primary_key=True, index=True)
text = Column(String, nullable=False)
#user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
source = Column(String, nullable=False, server_default=FetchedValue()) # 'user', 'assistant', or 'system'
source = Column(String, nullable=False, server_default='user') # 'user', 'assistant', or 'system'
bot_id = Column(Integer, ForeignKey("bots.id"), nullable=False)
created_at = Column(DateTime, default=func.now(), nullable=False)

Expand Down
8 changes: 4 additions & 4 deletions app/schemas/connector.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import List
from typing import List, Dict, Any
from pydantic import BaseModel

class ConnectorBase(BaseModel):
name: str
type: str
config: dict
description: str
config: Dict[str, Any]
connector_type: str

class ConnectorCreate(ConnectorBase):
pass
Expand Down
1 change: 1 addition & 0 deletions app/schemas/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class MessageInDBBase(MessageBase):
id: int
bot_id: int
created_at: datetime
source: str

class Config:
orm_mode = True
Expand Down
50 changes: 50 additions & 0 deletions app/static/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,53 @@ footer {
}


textarea {
width: 60vw; /* or any other width you want */
max-width: 500px; /* or any other width you want */
min-height: 50px; /* or any other minimum height you want */
resize: none; /* prevent manual resize */
overflow: hidden; /* prevent scrollbar */
}


.spinner {
display: inline-block;
width: 50px;
height: 50px;
border: 3px solid rgba(0, 0, 0, .3);
border-radius: 50%;
border-top-color: #000;
animation: spin 1s ease-in-out infinite;
-webkit-animation: spin 1s ease-in-out infinite;
}

@keyframes spin {
to { -webkit-transform: rotate(360deg); }
}

@-webkit-keyframes spin {
to { -webkit-transform: rotate(360deg); }
}


.message.system {
background-color: #f1f1f1;
}
.message.assistant {
background-color: #f0f0f0;
}

.message.user {
background-color: #ffffff;
}

.message-timestamp {
font-size: 0.8em;
text-align: right;
}

.copy-button {
font-size: 0.8em;
float: right;
}

40 changes: 32 additions & 8 deletions app/static/js/bots.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,23 @@ document.addEventListener('DOMContentLoaded', () => {

});

function selectBot(bot) {
const selectedBotNameElement = document.getElementById('selected-bot-name');
selectedBotNameElement.innerText = bot.name;
selectedBotNameElement.dataset.botId = bot.id; // Set the bot_id as a data attribute
}

sendButton.addEventListener("click", async (event) => {
event.preventDefault();

// Disable the button
sendButton.disabled = true;

// reset the enter dialog
const textarea = document.querySelector('textarea');
textarea.style.height = 'auto';

const spinner = document.getElementById('spinner');
const selectedBotNameElement = document.getElementById('selected-bot-name');
const content = textarea.value.trim();

spinner.style.display = 'inline-block';

if (selectedBotNameElement.innerText !== 'None' && content) {
const bot_id = selectedBotNameElement.dataset.botId; // Get the bot_id from the selected bot
await sendMessage(bot_id, content);
Expand All @@ -57,6 +59,8 @@ sendButton.addEventListener("click", async (event) => {

// Re-enable the button
sendButton.disabled = false;
spinner.style.display = 'none';

});

async function fetchBotsAndRender() {
Expand Down Expand Up @@ -118,6 +122,9 @@ function createBotElement(bot) {
selectedBotNameElement.innerText = bot.name;
selectedBotNameElement.setAttribute('data-bot-id', bot.id);
fetchMessagesAndRender(bot.id);
document.getElementById('send-message').removeAttribute('disabled');
document.getElementById('input-message').removeAttribute('disabled');
document.getElementById('input-message').placeholder = 'Enter your message...'
});


Expand Down Expand Up @@ -167,10 +174,12 @@ async function fetchMessagesAndRender(bot_id) {
messagesContainer.innerHTML = '';

for (const message of messages) {
///const messageHTML = marked(message.text);
const messageElement = createMessageElement(message);
messagesContainer.appendChild(messageElement);
}
messagesContainer.scrollTop = messagesContainer.scrollHeight;


}

async function sendMessage(bot_id, content) {
Expand All @@ -185,19 +194,34 @@ async function sendMessage(bot_id, content) {
return message;
}


function createMessageElement(message) {
const messageElement = document.createElement('div');
messageElement.className = 'message';
messageElement.className = 'message ' + message.source;
marked.setOptions({ mangle: false, headerIds: false });

const contentElement = document.createElement('p');
contentElement.className = 'message-content';
contentElement.innerText = message.text; // Changed from 'content' to 'text'
const messageWithTabsAndNewlinesReplaced = message.text.replace(/\t/g, "&nbsp;&nbsp;&nbsp;&nbsp;").replace(/\n/g, "<br/>");
const messageHTML = marked.marked(messageWithTabsAndNewlinesReplaced);
contentElement.innerHTML = messageHTML;
messageElement.appendChild(contentElement);

const timestampElement = document.createElement('p');
timestampElement.className = 'message-timestamp';
timestampElement.innerText = message.created_at; // Changed from 'timestamp' to 'created_at'

const copyButton = document.createElement('button');
copyButton.className = 'copy-button';
copyButton.textContent = 'Copy';
copyButton.addEventListener('click', function() {
navigator.clipboard.writeText(message.text);
});

messageElement.appendChild(timestampElement);
messageElement.appendChild(copyButton);
messageElement.appendChild(document.createElement('hr'));

return messageElement;
}

1 change: 1 addition & 0 deletions app/static/js/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ sendButton.addEventListener('click', () => {
});
});


24 changes: 19 additions & 5 deletions app/templates/bots.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ <h2>Selected Bot: <span id="selected-bot-name">None</span></h2>
<!-- Messages will be dynamically added here -->
</div>
<div class="input-message-container">
<textarea id="input-message" rows="1" placeholder="Type your message..." style="resize: none; overflow: hidden;"></textarea>
<textarea id="input-message" rows="1" placeholder="Pick a bot..." disabled></textarea>

<button id="send-message">Send</button>
<button id="send-message" disabled>Send</button>
<div id="spinner" class="spinner" style="display: none;"></div>
</div>

</div>
Expand All @@ -41,7 +42,19 @@ <h2>Selected Bot: <span id="selected-bot-name">None</span></h2>
<script>


var textarea = document.getElementById('input-message');
var textarea = document.getElementById('input-message');

textarea.addEventListener('input', () => {
// Temporarily set the height to 'auto' so the scrollHeight property will give us the full height of the content
textarea.style.height = 'auto';

// Now set the height to the content's full height (but not more than max-height)
// The 'Math.min' part is not necessary if you don't have a max-height
textarea.style.height = Math.min(textarea.scrollHeight, 300) + 'px';
});
textarea.dispatchEvent(new Event('input'));


var sendButton = document.getElementById("send-message");
textarea.oninput = function() {
var lines = textarea.value.split('\n');
Expand All @@ -53,10 +66,11 @@ <h2>Selected Bot: <span id="selected-bot-name">None</span></h2>
}
};


</script>

<script src="/static/js/marked.min.js"></script>
<script src="/static/js/bots.js"></script>
<script src="/static/js/marked.min.js" defer></script>
<script src="/static/js/bots.js" defer></script>
{% endblock %}


Loading

0 comments on commit 14ff44e

Please sign in to comment.